Merge remote-tracking branch 'origin/dev' into sub_laptop_7
This commit is contained in:
@@ -360,16 +360,23 @@ export async function searchMemberCandidates(query: string): Promise<Member[]> {
|
||||
}
|
||||
|
||||
/** 이미 가입한 사람을 회사에 붙인다 — 계정을 대신 만들지 않는다. */
|
||||
export function addCompanyMember(userId: number): Promise<{ member: Member }> {
|
||||
return request("/dashboard/admin/members", {
|
||||
export function addCompanyMember(
|
||||
userId: number,
|
||||
companyId?: number | null,
|
||||
): Promise<{ member: Member }> {
|
||||
return request(`/dashboard/admin/members${companyQuery(companyId)}`, {
|
||||
method: "POST",
|
||||
body: body({ user_id: userId }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 아직 가입하지 않은 사람에게 가입 안내 메일만 보낸다. */
|
||||
export function inviteMember(email: string, name?: string | null): Promise<unknown> {
|
||||
return request("/dashboard/admin/members/invite", {
|
||||
export function inviteMember(
|
||||
email: string,
|
||||
name?: string | null,
|
||||
companyId?: number | null,
|
||||
): Promise<unknown> {
|
||||
return request(`/dashboard/admin/members/invite${companyQuery(companyId)}`, {
|
||||
method: "POST",
|
||||
body: body({ email, name: name || null }),
|
||||
});
|
||||
@@ -395,8 +402,11 @@ export function removeCompanyMember(userId: number): Promise<unknown> {
|
||||
return request(`/dashboard/admin/members/${userId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function fetchCompanyJoinRequests(): Promise<JoinRequest[]> {
|
||||
const data = await request<{ requests: JoinRequest[] }>("/dashboard/admin/join-requests");
|
||||
/** companyId 는 시스템관리자가 대시보드에서 고른 회사(29-1) — 없으면 자기 회사. */
|
||||
export async function fetchCompanyJoinRequests(companyId?: number | null): Promise<JoinRequest[]> {
|
||||
const data = await request<{ requests: JoinRequest[] }>(
|
||||
`/dashboard/admin/join-requests${companyQuery(companyId)}`,
|
||||
);
|
||||
return data.requests;
|
||||
}
|
||||
|
||||
@@ -410,8 +420,10 @@ export function processJoinRequest(
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchCompanyProjects(): Promise<ProjectItem[]> {
|
||||
const data = await request<{ projects: ProjectItem[] }>("/dashboard/admin/projects");
|
||||
export async function fetchCompanyProjects(companyId?: number | null): Promise<ProjectItem[]> {
|
||||
const data = await request<{ projects: ProjectItem[] }>(
|
||||
`/dashboard/admin/projects${companyQuery(companyId)}`,
|
||||
);
|
||||
return data.projects;
|
||||
}
|
||||
|
||||
@@ -425,8 +437,10 @@ export async function fetchAllCompanies(): Promise<CompanyInfo[]> {
|
||||
return data.companies;
|
||||
}
|
||||
|
||||
export async function fetchAllUsers(): Promise<DashboardUser[]> {
|
||||
const data = await request<{ users: DashboardUser[] }>("/dashboard/admin/users");
|
||||
export async function fetchAllUsers(companyId?: number | null): Promise<DashboardUser[]> {
|
||||
const data = await request<{ users: DashboardUser[] }>(
|
||||
`/dashboard/admin/users${companyQuery(companyId)}`,
|
||||
);
|
||||
return data.users;
|
||||
}
|
||||
|
||||
|
||||
@@ -338,15 +338,17 @@ async def soft_delete_project(project_id: str, actor_id: int, request: Any | Non
|
||||
return changed
|
||||
|
||||
|
||||
async def list_all_users() -> list[dict[str, Any]]:
|
||||
async def list_all_users(company_id: int | None = None) -> list[dict[str, Any]]:
|
||||
where = " AND u.company_id = %s" if company_id else ""
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"""SELECT u.id, u.email, u.name, u.position, u.department, u.phone,
|
||||
f"""SELECT u.id, u.email, u.name, u.position, u.department, u.phone,
|
||||
u.role, u.status, u.company_id,
|
||||
c.name AS company_name, u.last_login
|
||||
FROM users u LEFT JOIN companies c ON c.id = u.company_id
|
||||
WHERE u.deleted_at IS NULL ORDER BY u.created_at DESC LIMIT 200"""
|
||||
WHERE u.deleted_at IS NULL{where} ORDER BY u.created_at DESC LIMIT 200""",
|
||||
(company_id,) if company_id else (),
|
||||
)
|
||||
rows = list(await cursor.fetchall())
|
||||
for row in rows:
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
"""B01_Dashboard 역할별 대시보드 API."""
|
||||
|
||||
import html
|
||||
import mimetypes
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
File,
|
||||
Form,
|
||||
HTTPException,
|
||||
Path,
|
||||
Query,
|
||||
Request,
|
||||
Response,
|
||||
UploadFile,
|
||||
)
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, Response
|
||||
from pymysql.err import IntegrityError
|
||||
|
||||
from common_util.common_util_auth import require_company, require_system_admin, verify_session
|
||||
from common_util.common_util_email import send_email
|
||||
from common_util.common_util_project_delete import hard_delete_project
|
||||
from common_util.common_util_storage import read_stored_asset
|
||||
from config.config_system import APP_PUBLIC_BASE_URL, PROJECT_DELETE_HARD_ENABLED
|
||||
|
||||
from .B01_Dashboard_Map import fetch_base_tile, geocode_address, search_address
|
||||
@@ -47,17 +33,6 @@ from .B01_Dashboard_Repository import (
|
||||
update_project,
|
||||
update_user_profile,
|
||||
)
|
||||
from .B01_Dashboard_Repository_Assets import (
|
||||
ASSET_EXTENSIONS,
|
||||
ASSET_KINDS,
|
||||
ASSET_MAX_BYTES,
|
||||
create_company_asset,
|
||||
delete_company_asset,
|
||||
get_company_asset,
|
||||
list_company_assets,
|
||||
update_company_asset,
|
||||
write_company_asset_file,
|
||||
)
|
||||
from .B01_Dashboard_Repository_Company import (
|
||||
create_company,
|
||||
get_user_company,
|
||||
@@ -75,9 +50,9 @@ from .B01_Dashboard_Repository_Members import (
|
||||
list_company_members,
|
||||
list_unassigned_users,
|
||||
remove_company_member,
|
||||
set_company_logo,
|
||||
set_project_members,
|
||||
)
|
||||
from .B01_Dashboard_Router_Assets import router as assets_router
|
||||
from .B01_Dashboard_Schema import (
|
||||
AddMemberRequest,
|
||||
AdminUpdateUserRequest,
|
||||
@@ -88,52 +63,19 @@ from .B01_Dashboard_Schema import (
|
||||
JoinCompanyRequest,
|
||||
ProcessJoinRequest,
|
||||
UiPrefsRequest,
|
||||
UpdateCompanyAssetRequest,
|
||||
UpdateCompanyLogoRequest,
|
||||
UpdateProjectRequest,
|
||||
UpdateUserRequest,
|
||||
)
|
||||
from .B01_Dashboard_Scope import (
|
||||
_require_company_id,
|
||||
_same_company,
|
||||
_scope_company,
|
||||
require_company_admin,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["B01_Dashboard"])
|
||||
|
||||
|
||||
async def require_company_admin(
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> dict[str, Any]:
|
||||
if session["role"] not in ("ADMIN", "SYSTEM_ADMIN") and not session["is_master"]:
|
||||
raise HTTPException(status_code=403, detail="회사 관리자 권한이 필요합니다.")
|
||||
return session
|
||||
|
||||
|
||||
def _require_company_id(session: dict[str, Any]) -> int:
|
||||
company_id = session.get("company_id")
|
||||
if company_id is None:
|
||||
raise HTTPException(status_code=403, detail="회사 연결이 필요합니다.")
|
||||
return int(company_id)
|
||||
|
||||
|
||||
def _same_company(session: dict[str, Any], company_id: int | None) -> bool:
|
||||
return company_id is not None and int(session.get("company_id") or 0) == int(company_id)
|
||||
|
||||
|
||||
def _scope_company(session: dict[str, Any], company_id: int | None) -> int:
|
||||
"""회사 범위 자원(구성원·로고·서명)의 회사. 시스템관리자만 남의 회사를 지정할 수 있다."""
|
||||
if session["role"] == "SYSTEM_ADMIN" and company_id is not None:
|
||||
return int(company_id)
|
||||
own = _require_company_id(session)
|
||||
if company_id is not None and int(company_id) != own:
|
||||
raise HTTPException(status_code=403, detail="다른 회사의 자료는 볼 수 없습니다.")
|
||||
return own
|
||||
|
||||
|
||||
async def _company_asset(session: dict[str, Any], asset_id: int) -> dict[str, Any]:
|
||||
asset = await get_company_asset(asset_id)
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="자산을 찾을 수 없습니다.")
|
||||
_scope_company(session, int(asset["company_id"]))
|
||||
return asset
|
||||
|
||||
|
||||
async def _can_edit_project(session: dict[str, Any], project: dict[str, Any]) -> bool:
|
||||
if session["role"] == "SYSTEM_ADMIN":
|
||||
return True
|
||||
@@ -326,9 +268,10 @@ async def admin_member_candidates(
|
||||
@router.post("/admin/members")
|
||||
async def admin_add_member(
|
||||
payload: AddMemberRequest,
|
||||
company_id: int | None = Query(None, gt=0),
|
||||
session: dict[str, Any] = Depends(require_company_admin),
|
||||
):
|
||||
member = await attach_company_member(_require_company_id(session), payload.user_id)
|
||||
member = await attach_company_member(_scope_company(session, company_id), payload.user_id)
|
||||
if not member:
|
||||
raise HTTPException(
|
||||
status_code=409, detail="이미 다른 회사 소속이거나 찾을 수 없는 사용자입니다."
|
||||
@@ -339,10 +282,11 @@ async def admin_add_member(
|
||||
@router.post("/admin/members/invite")
|
||||
async def admin_invite_member(
|
||||
payload: InviteMemberRequest,
|
||||
company_id: int | None = Query(None, gt=0),
|
||||
session: dict[str, Any] = Depends(require_company_admin),
|
||||
):
|
||||
"""아직 가입하지 않은 사람에게 안내 메일만 보낸다 — 계정을 대신 만들지 않는다."""
|
||||
company = await get_user_company(session.get("company_id"))
|
||||
company = await get_user_company(_scope_company(session, company_id))
|
||||
company_name = (company or {}).get("name") or "회사"
|
||||
# 메일 본문에 사람이 넣은 글자가 그대로 들어가면 남의 편지함에 임의 HTML 을 보낼 수 있다.
|
||||
safe_company = html.escape(company_name)
|
||||
@@ -369,8 +313,12 @@ async def admin_remove_member(
|
||||
|
||||
|
||||
@router.get("/admin/join-requests")
|
||||
async def admin_join_requests(session: dict[str, Any] = Depends(require_company_admin)):
|
||||
return {"status": "success", "requests": await list_join_requests(_require_company_id(session))}
|
||||
async def admin_join_requests(
|
||||
company_id: int | None = Query(None, gt=0),
|
||||
session: dict[str, Any] = Depends(require_company_admin),
|
||||
):
|
||||
scoped = _scope_company(session, company_id)
|
||||
return {"status": "success", "requests": await list_join_requests(scoped)}
|
||||
|
||||
|
||||
@router.patch("/admin/join-requests/{request_id}")
|
||||
@@ -392,10 +340,13 @@ async def admin_process_join_request(
|
||||
|
||||
|
||||
@router.get("/admin/projects")
|
||||
async def admin_projects(session: dict[str, Any] = Depends(require_company_admin)):
|
||||
async def admin_projects(
|
||||
company_id: int | None = Query(None, gt=0),
|
||||
session: dict[str, Any] = Depends(require_company_admin),
|
||||
):
|
||||
return {
|
||||
"status": "success",
|
||||
"projects": await list_company_projects(_require_company_id(session)),
|
||||
"projects": await list_company_projects(_scope_company(session, company_id)),
|
||||
}
|
||||
|
||||
|
||||
@@ -470,9 +421,13 @@ async def system_create_company(
|
||||
|
||||
|
||||
@router.get("/admin/users")
|
||||
async def system_users(session: dict[str, Any] = Depends(require_system_admin)):
|
||||
async def system_users(
|
||||
company_id: int | None = Query(None, gt=0),
|
||||
session: dict[str, Any] = Depends(require_system_admin),
|
||||
):
|
||||
"""company_id = 대시보드에서 고른 회사 사람만(29-1) · 없으면 전체."""
|
||||
_ = session
|
||||
return {"status": "success", "users": await list_all_users()}
|
||||
return {"status": "success", "users": await list_all_users(company_id)}
|
||||
|
||||
|
||||
@router.patch("/admin/users/{user_id}/role")
|
||||
@@ -607,95 +562,4 @@ async def system_projects(session: dict[str, Any] = Depends(require_system_admin
|
||||
return {"status": "success", "projects": await list_all_projects()}
|
||||
|
||||
|
||||
# ---- 회사 공유 도면 자산(로고·서명) — 회사 구성원 누구나 작성·수정, 시스템관리자는 전체 ----
|
||||
|
||||
|
||||
@router.get("/company/assets")
|
||||
async def company_assets(
|
||||
company_id: int | None = Query(None, gt=0),
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
):
|
||||
return {
|
||||
"status": "success",
|
||||
"assets": await list_company_assets(_scope_company(session, company_id)),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/company/assets")
|
||||
async def company_add_asset(
|
||||
kind: str = Form(...),
|
||||
label: str = Form(...),
|
||||
user_id: int | None = Form(None, gt=0),
|
||||
company_id: int | None = Form(None, gt=0),
|
||||
file: UploadFile = File(...),
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
):
|
||||
kind = kind.upper()
|
||||
if kind not in ASSET_KINDS:
|
||||
raise HTTPException(status_code=400, detail="자산 종류는 LOGO 또는 SIGNATURE 입니다.")
|
||||
label = label.strip()[:100]
|
||||
if not label:
|
||||
raise HTTPException(status_code=400, detail="이름을 넣어 주세요.")
|
||||
suffix = os.path.splitext(file.filename or "")[1].lower()
|
||||
if suffix not in ASSET_EXTENSIONS:
|
||||
raise HTTPException(status_code=400, detail="png·jpg·webp·svg 그림만 올릴 수 있습니다.")
|
||||
blob = await file.read()
|
||||
if not blob or len(blob) > ASSET_MAX_BYTES:
|
||||
raise HTTPException(status_code=400, detail="그림은 2MB 이하여야 합니다.")
|
||||
scoped = _scope_company(session, company_id)
|
||||
if user_id and user_id not in {m["id"] for m in await list_company_members(scoped)}:
|
||||
raise HTTPException(status_code=400, detail="주인은 같은 회사 구성원이어야 합니다.")
|
||||
path = write_company_asset_file(scoped, kind, suffix, blob)
|
||||
asset_id = await create_company_asset(
|
||||
scoped, kind, label, path, user_id, int(session["user_id"])
|
||||
)
|
||||
return {"status": "success", "asset_id": asset_id}
|
||||
|
||||
|
||||
@router.put("/company/assets/{asset_id}")
|
||||
async def company_update_asset(
|
||||
asset_id: int,
|
||||
payload: UpdateCompanyAssetRequest,
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
):
|
||||
await _company_asset(session, asset_id)
|
||||
await update_company_asset(asset_id, payload.label.strip(), payload.user_id)
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.delete("/company/assets/{asset_id}")
|
||||
async def company_delete_asset(asset_id: int, session: dict[str, Any] = Depends(verify_session)):
|
||||
await _company_asset(session, asset_id)
|
||||
await delete_company_asset(asset_id)
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.put("/company/logo")
|
||||
async def company_set_logo(
|
||||
payload: UpdateCompanyLogoRequest,
|
||||
company_id: int | None = Query(None, gt=0),
|
||||
session: dict[str, Any] = Depends(require_company_admin),
|
||||
):
|
||||
"""회사 대표 로고를 지정·변경한다 (2026-09-02 사용자 확정).
|
||||
|
||||
프로젝트가 따로 고르지 않으면 도면은 이 로고를 쓴다.
|
||||
"""
|
||||
scoped = _scope_company(session, company_id)
|
||||
if payload.logo_asset_id is not None:
|
||||
asset = await get_company_asset(payload.logo_asset_id)
|
||||
if not asset or asset["company_id"] != scoped or asset["kind"] != "LOGO":
|
||||
raise HTTPException(status_code=400, detail="같은 회사의 로고 자산이어야 합니다.")
|
||||
if not await set_company_logo(scoped, payload.logo_asset_id):
|
||||
raise HTTPException(status_code=404, detail="회사를 찾을 수 없습니다.")
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.get("/company/assets/{asset_id}/file")
|
||||
async def company_asset_file(asset_id: int, session: dict[str, Any] = Depends(verify_session)):
|
||||
"""목록 미리보기용 그림. 도면에는 B07 이 같은 파일을 data URL 로 심는다."""
|
||||
asset = await _company_asset(session, asset_id)
|
||||
blob = read_stored_asset(asset["file_path"])
|
||||
if blob is None:
|
||||
raise HTTPException(status_code=404, detail="그림 파일이 없습니다.")
|
||||
media_type = mimetypes.guess_type(asset["file_path"])[0] or "application/octet-stream"
|
||||
return Response(content=blob, media_type=media_type)
|
||||
router.include_router(assets_router)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""B01_Dashboard 회사 공유 도면 자산(로고·서명) API — 본체 라우터가 include 함."""
|
||||
|
||||
import mimetypes
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Response, UploadFile
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from common_util.common_util_storage import read_stored_asset
|
||||
|
||||
from .B01_Dashboard_Repository_Assets import (
|
||||
ASSET_EXTENSIONS,
|
||||
ASSET_KINDS,
|
||||
ASSET_MAX_BYTES,
|
||||
create_company_asset,
|
||||
delete_company_asset,
|
||||
get_company_asset,
|
||||
list_company_assets,
|
||||
update_company_asset,
|
||||
write_company_asset_file,
|
||||
)
|
||||
from .B01_Dashboard_Repository_Members import list_company_members, set_company_logo
|
||||
from .B01_Dashboard_Schema import UpdateCompanyAssetRequest, UpdateCompanyLogoRequest
|
||||
from .B01_Dashboard_Scope import _scope_company, require_company_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def _company_asset(session: dict[str, Any], asset_id: int) -> dict[str, Any]:
|
||||
asset = await get_company_asset(asset_id)
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="자산을 찾을 수 없습니다.")
|
||||
_scope_company(session, int(asset["company_id"]))
|
||||
return asset
|
||||
|
||||
|
||||
# ---- 회사 공유 도면 자산(로고·서명) — 회사 구성원 누구나 작성·수정, 시스템관리자는 전체 ----
|
||||
|
||||
|
||||
@router.get("/company/assets")
|
||||
async def company_assets(
|
||||
company_id: int | None = Query(None, gt=0),
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
):
|
||||
return {
|
||||
"status": "success",
|
||||
"assets": await list_company_assets(_scope_company(session, company_id)),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/company/assets")
|
||||
async def company_add_asset(
|
||||
kind: str = Form(...),
|
||||
label: str = Form(...),
|
||||
user_id: int | None = Form(None, gt=0),
|
||||
company_id: int | None = Form(None, gt=0),
|
||||
file: UploadFile = File(...),
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
):
|
||||
kind = kind.upper()
|
||||
if kind not in ASSET_KINDS:
|
||||
raise HTTPException(status_code=400, detail="자산 종류는 LOGO 또는 SIGNATURE 입니다.")
|
||||
label = label.strip()[:100]
|
||||
if not label:
|
||||
raise HTTPException(status_code=400, detail="이름을 넣어 주세요.")
|
||||
suffix = os.path.splitext(file.filename or "")[1].lower()
|
||||
if suffix not in ASSET_EXTENSIONS:
|
||||
raise HTTPException(status_code=400, detail="png·jpg·webp·svg 그림만 올릴 수 있습니다.")
|
||||
blob = await file.read()
|
||||
if not blob or len(blob) > ASSET_MAX_BYTES:
|
||||
raise HTTPException(status_code=400, detail="그림은 2MB 이하여야 합니다.")
|
||||
scoped = _scope_company(session, company_id)
|
||||
if user_id and user_id not in {m["id"] for m in await list_company_members(scoped)}:
|
||||
raise HTTPException(status_code=400, detail="주인은 같은 회사 구성원이어야 합니다.")
|
||||
path = write_company_asset_file(scoped, kind, suffix, blob)
|
||||
asset_id = await create_company_asset(
|
||||
scoped, kind, label, path, user_id, int(session["user_id"])
|
||||
)
|
||||
return {"status": "success", "asset_id": asset_id}
|
||||
|
||||
|
||||
@router.put("/company/assets/{asset_id}")
|
||||
async def company_update_asset(
|
||||
asset_id: int,
|
||||
payload: UpdateCompanyAssetRequest,
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
):
|
||||
await _company_asset(session, asset_id)
|
||||
await update_company_asset(asset_id, payload.label.strip(), payload.user_id)
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.delete("/company/assets/{asset_id}")
|
||||
async def company_delete_asset(asset_id: int, session: dict[str, Any] = Depends(verify_session)):
|
||||
await _company_asset(session, asset_id)
|
||||
await delete_company_asset(asset_id)
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.put("/company/logo")
|
||||
async def company_set_logo(
|
||||
payload: UpdateCompanyLogoRequest,
|
||||
company_id: int | None = Query(None, gt=0),
|
||||
session: dict[str, Any] = Depends(require_company_admin),
|
||||
):
|
||||
"""회사 대표 로고를 지정·변경한다 (2026-09-02 사용자 확정).
|
||||
|
||||
프로젝트가 따로 고르지 않으면 도면은 이 로고를 쓴다.
|
||||
"""
|
||||
scoped = _scope_company(session, company_id)
|
||||
if payload.logo_asset_id is not None:
|
||||
asset = await get_company_asset(payload.logo_asset_id)
|
||||
if not asset or asset["company_id"] != scoped or asset["kind"] != "LOGO":
|
||||
raise HTTPException(status_code=400, detail="같은 회사의 로고 자산이어야 합니다.")
|
||||
if not await set_company_logo(scoped, payload.logo_asset_id):
|
||||
raise HTTPException(status_code=404, detail="회사를 찾을 수 없습니다.")
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.get("/company/assets/{asset_id}/file")
|
||||
async def company_asset_file(asset_id: int, session: dict[str, Any] = Depends(verify_session)):
|
||||
"""목록 미리보기용 그림. 도면에는 B07 이 같은 파일을 data URL 로 심는다."""
|
||||
asset = await _company_asset(session, asset_id)
|
||||
blob = read_stored_asset(asset["file_path"])
|
||||
if blob is None:
|
||||
raise HTTPException(status_code=404, detail="그림 파일이 없습니다.")
|
||||
media_type = mimetypes.guess_type(asset["file_path"])[0] or "application/octet-stream"
|
||||
return Response(content=blob, media_type=media_type)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""B01_Dashboard 권한 · 회사 범위 도우미 — 라우터 둘(본체 · 자산)이 함께 씀."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
|
||||
|
||||
async def require_company_admin(
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> dict[str, Any]:
|
||||
if session["role"] not in ("ADMIN", "SYSTEM_ADMIN") and not session["is_master"]:
|
||||
raise HTTPException(status_code=403, detail="회사 관리자 권한이 필요합니다.")
|
||||
return session
|
||||
|
||||
|
||||
def _require_company_id(session: dict[str, Any]) -> int:
|
||||
company_id = session.get("company_id")
|
||||
if company_id is None:
|
||||
raise HTTPException(status_code=403, detail="회사 연결이 필요합니다.")
|
||||
return int(company_id)
|
||||
|
||||
|
||||
def _same_company(session: dict[str, Any], company_id: int | None) -> bool:
|
||||
return company_id is not None and int(session.get("company_id") or 0) == int(company_id)
|
||||
|
||||
|
||||
def _scope_company(session: dict[str, Any], company_id: int | None) -> int:
|
||||
"""회사 범위 자원의 회사 — 시스템관리자면 인자 우선(대시보드 회사 고르기) · 남은 세션 회사."""
|
||||
if session["role"] == "SYSTEM_ADMIN" and company_id is not None:
|
||||
return int(company_id)
|
||||
own = _require_company_id(session)
|
||||
if company_id is not None and int(company_id) != own:
|
||||
raise HTTPException(status_code=403, detail="다른 회사의 자료는 볼 수 없습니다.")
|
||||
return own
|
||||
@@ -584,7 +584,10 @@ export function openFindCompanyModal(): void {
|
||||
* (2026-09-06 사용자 확정). 계정을 대신 만들지 않는다. 아직 가입하지 않은 사람에게는
|
||||
* 안내 메일만 보낸다.
|
||||
*/
|
||||
export function openAddMemberModal(onCreated?: (member: Member) => void): void {
|
||||
export function openAddMemberModal(
|
||||
onCreated?: (member: Member) => void,
|
||||
companyId?: number | null,
|
||||
): void {
|
||||
const query = createInputField({
|
||||
label: "이름 또는 이메일로 찾기",
|
||||
placeholder: "두 글자 이상",
|
||||
@@ -638,7 +641,7 @@ export function openAddMemberModal(onCreated?: (member: Member) => void): void {
|
||||
return;
|
||||
}
|
||||
query.setError();
|
||||
await inviteMember(text);
|
||||
await inviteMember(text, null, companyId);
|
||||
showToast("안내 메일을 보냈습니다.", "success");
|
||||
},
|
||||
});
|
||||
@@ -652,7 +655,7 @@ export function openAddMemberModal(onCreated?: (member: Member) => void): void {
|
||||
query.setError("등록할 사람을 먼저 고르십시오.");
|
||||
throw new Error("등록할 사람을 먼저 고르십시오.");
|
||||
}
|
||||
const created = await addCompanyMember(picked.id);
|
||||
const created = await addCompanyMember(picked.id, companyId);
|
||||
showToast(L("B01_Dashboard_Saved"), "success");
|
||||
if (created?.member) onCreated?.(created.member);
|
||||
});
|
||||
|
||||
@@ -7,13 +7,12 @@ import {
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createMapContextMenu, type MapContextMenu } from "@ui/ui_template_context_menu";
|
||||
import { section } from "@ui/ui_template_general_blocks";
|
||||
import { createGeneralLayout } from "@ui/ui_template_general_layout";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import {
|
||||
fetchAllCompanies,
|
||||
fetchAllJoinRequests,
|
||||
fetchAllProjects,
|
||||
fetchAllUsers,
|
||||
fetchAuditLogs,
|
||||
fetchCompanyJoinRequests,
|
||||
@@ -59,6 +58,18 @@ export interface DashboardState {
|
||||
allJoinRequests: JoinRequest[];
|
||||
auditLogs: AuditLog[];
|
||||
resources: ResourceData | null;
|
||||
/** 시스템 관리자가 고른 회사(29-1) — 프로젝트 · 사용자 · 가입 요청 · 회사 관리가 이 회사 것 */
|
||||
adminCompanyId: number | null;
|
||||
}
|
||||
|
||||
/** 고른 회사 id 자리 — M02 도 같은 키를 읽음(이름 바꾸지 않음) */
|
||||
const ADMIN_COMPANY_KEY = "aislo.adminCompany";
|
||||
|
||||
/** 고른 회사 — 저장값이 없거나 없는 회사면 자기 회사 */
|
||||
function pickAdminCompany(user: DashboardUser, companies: CompanyInfo[]): number | null {
|
||||
const saved = Number(sessionStorage.getItem(ADMIN_COMPANY_KEY));
|
||||
if (companies.some((c) => c.id === saved)) return saved;
|
||||
return user.company_id ?? companies[0]?.id ?? null;
|
||||
}
|
||||
|
||||
export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
||||
@@ -78,6 +89,7 @@ export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
||||
allJoinRequests: [],
|
||||
auditLogs: [],
|
||||
resources: null,
|
||||
adminCompanyId: null,
|
||||
};
|
||||
await loadRoleData(state);
|
||||
root.innerHTML = "";
|
||||
@@ -91,20 +103,22 @@ export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
||||
|
||||
async function loadRoleData(state: DashboardState): Promise<void> {
|
||||
if (state.user.role === "SYSTEM_ADMIN") {
|
||||
// 회사 목록을 먼저 — 고른 회사를 가린 뒤 그 회사 것만 읽음(29-1)
|
||||
state.allCompanies = await fetchAllCompanies().catch(() => []);
|
||||
const companyId = pickAdminCompany(state.user, state.allCompanies);
|
||||
state.adminCompanyId = companyId;
|
||||
const results = await Promise.allSettled([
|
||||
fetchAllCompanies(),
|
||||
fetchAllUsers(),
|
||||
fetchAllJoinRequests(),
|
||||
fetchAllUsers(companyId),
|
||||
fetchCompanyJoinRequests(companyId),
|
||||
fetchAuditLogs(),
|
||||
fetchSystemResources(7),
|
||||
fetchAllProjects(),
|
||||
fetchCompanyProjects(companyId),
|
||||
]);
|
||||
state.allCompanies = results[0].status === "fulfilled" ? results[0].value : [];
|
||||
state.allUsers = results[1].status === "fulfilled" ? results[1].value : [];
|
||||
state.allJoinRequests = results[2].status === "fulfilled" ? results[2].value : [];
|
||||
state.auditLogs = results[3].status === "fulfilled" ? results[3].value : [];
|
||||
state.resources = results[4].status === "fulfilled" ? results[4].value : null;
|
||||
state.allProjects = results[5].status === "fulfilled" ? results[5].value : [];
|
||||
state.allUsers = results[0].status === "fulfilled" ? results[0].value : [];
|
||||
state.allJoinRequests = results[1].status === "fulfilled" ? results[1].value : [];
|
||||
state.auditLogs = results[2].status === "fulfilled" ? results[2].value : [];
|
||||
state.resources = results[3].status === "fulfilled" ? results[3].value : null;
|
||||
state.allProjects = results[4].status === "fulfilled" ? results[4].value : [];
|
||||
|
||||
results.forEach((res, idx) => {
|
||||
if (res.status === "rejected") {
|
||||
@@ -148,11 +162,16 @@ function settingCard(title: string, desc: string, go: () => void): HTMLElement {
|
||||
}
|
||||
|
||||
/** 시스템 설정 — 역할로 카드 숨김 · M02 카드는 `?open=&root=` 로 진입(27-1d) */
|
||||
function buildSystemSettingsPanel(user: DashboardUser): HTMLElement {
|
||||
function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b01-dashboard__setting-grid";
|
||||
// 시스템 관리자는 고른 회사를 해시에 실음(29-1)
|
||||
const company: Record<string, string> =
|
||||
user.role === "SYSTEM_ADMIN" && companyId ? { company: String(companyId) } : {};
|
||||
const m02 = (title: string, open: string, root: string, desc: string): HTMLElement =>
|
||||
settingCard(title, desc, () => navigateTo(ROUTES.M02_MASTER_TEMPLATE, { open, root }));
|
||||
settingCard(title, desc, () =>
|
||||
navigateTo(ROUTES.M02_MASTER_TEMPLATE, { open, root, ...company }),
|
||||
);
|
||||
if (user.role === "SYSTEM_ADMIN") {
|
||||
const desc = L("B01_Dashboard_PermMasterWrite");
|
||||
wrap.append(
|
||||
@@ -181,9 +200,46 @@ function buildSystemSettingsPanel(user: DashboardUser): HTMLElement {
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/** 회사 메뉴 — 공용 작은 메뉴 하나를 알약 모두가 같이 씀 */
|
||||
let companyMenu: MapContextMenu | null = null;
|
||||
|
||||
/** 회사 알약(시스템 관리자만) — 누르면 등록된 회사 메뉴 · 고르면 저장 뒤 대시보드를 다시 읽음 */
|
||||
function companyPill(state: DashboardState): HTMLElement {
|
||||
const name = state.allCompanies.find((c) => c.id === state.adminCompanyId)?.name ?? "-";
|
||||
const pill = document.createElement("button");
|
||||
pill.type = "button";
|
||||
pill.className = "ui-tag ui-tag--accent b01-dashboard__company-pill";
|
||||
pill.title = L("B01_Dashboard_PickCompany");
|
||||
pill.textContent = `${name} ▾`;
|
||||
pill.addEventListener("click", (event) => {
|
||||
// 제목 줄 누름 = 접힘 — 알약 누름은 올려 보내지 않음
|
||||
event.stopPropagation();
|
||||
if (!companyMenu?.element.isConnected) {
|
||||
companyMenu = createMapContextMenu("b01-dashboard");
|
||||
document.body.append(companyMenu.element);
|
||||
}
|
||||
const box = pill.getBoundingClientRect();
|
||||
companyMenu.open(
|
||||
box.left,
|
||||
box.bottom + 4,
|
||||
state.allCompanies.map((c): [string, () => void] => [
|
||||
c.name,
|
||||
() => {
|
||||
sessionStorage.setItem(ADMIN_COMPANY_KEY, String(c.id));
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
return pill;
|
||||
}
|
||||
|
||||
/** 시스템 설정 컨테이너 — 모든 역할 · 한 줄 통째 · 제목 눌러 통째로 접힘 */
|
||||
function buildSystemSettingsSection(user: DashboardUser): HTMLElement {
|
||||
const card = section(L("B01_Dashboard_SystemSettings"), buildSystemSettingsPanel(user), true);
|
||||
function buildSystemSettingsSection(state: DashboardState): HTMLElement {
|
||||
const { user } = state;
|
||||
const pill = user.role === "SYSTEM_ADMIN" ? [companyPill(state)] : [];
|
||||
const panel = buildSystemSettingsPanel(user, state.adminCompanyId);
|
||||
const card = section(L("B01_Dashboard_SystemSettings"), panel, true, pill);
|
||||
card.querySelector(".ui-card__body")?.classList.add("ui-collapsible");
|
||||
card.querySelector(".b01-dashboard__section-header")?.classList.add("ui-collapsible__title");
|
||||
attachCollapsible(card);
|
||||
@@ -196,26 +252,40 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "b01-dashboard__grid";
|
||||
// 시스템 설정은 모든 역할 맨 위 한 줄(27-3a)
|
||||
grid.append(buildSystemSettingsSection(state.user));
|
||||
|
||||
if (state.user.role === "SYSTEM_ADMIN") {
|
||||
// 시스템 설정은 리소스 현황 아래(29-1) · 회사 알약이 붙은 컨테이너는 고른 회사 것
|
||||
const picked = state.allCompanies.filter((c) => c.id === state.adminCompanyId);
|
||||
grid.append(
|
||||
section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true),
|
||||
buildSystemSettingsSection(state),
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [
|
||||
companyPill(state),
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
]),
|
||||
buildTempUploadSection(),
|
||||
section(L("B01_Dashboard_Users"), userTable(state.allUsers, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => openAddMemberModal() }),
|
||||
companyPill(state),
|
||||
createButton({
|
||||
label: "+",
|
||||
onClick: () => openAddMemberModal(undefined, state.adminCompanyId),
|
||||
}),
|
||||
]),
|
||||
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.allJoinRequests, true), true),
|
||||
section(L("B01_Dashboard_Companies"), companyTable(state.allCompanies, state.user), true, [
|
||||
section(
|
||||
L("B01_Dashboard_JoinRequests"),
|
||||
joinRequestTable(state.allJoinRequests, true),
|
||||
true,
|
||||
[companyPill(state)],
|
||||
),
|
||||
section(L("B01_Dashboard_Companies"), companyTable(picked, state.user), true, [
|
||||
companyPill(state),
|
||||
createButton({ label: "+", onClick: () => openCreateCompanyModal() }),
|
||||
]),
|
||||
);
|
||||
} else if (state.user.role === "ADMIN") {
|
||||
// 시스템 설정은 맨 위 한 줄(27-3a)
|
||||
grid.append(
|
||||
buildSystemSettingsSection(state),
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
]),
|
||||
@@ -228,6 +298,7 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
);
|
||||
} else {
|
||||
grid.append(
|
||||
buildSystemSettingsSection(state),
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
]),
|
||||
|
||||
@@ -68,6 +68,59 @@
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 회사 알약 — 회사를 따라 바뀌는 컨테이너 제목 오른쪽 · 누르면 회사 메뉴 */
|
||||
.b01-dashboard__company-pill {
|
||||
border: 0;
|
||||
font: inherit;
|
||||
font-size: var(--text-caption);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b01-dashboard__context-menu {
|
||||
position: fixed;
|
||||
z-index: var(--z-modal);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 160px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.b01-dashboard__context-menu[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b01-dashboard__context-menu-item {
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border: 0;
|
||||
border-radius: var(--radius-cards);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: var(--text-body-sm);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b01-dashboard__context-menu-item:hover {
|
||||
background: var(--color-surface-sunken);
|
||||
}
|
||||
|
||||
/* 접히는 제목 줄 — 단추 칸을 접힘 표시 바로 왼쪽으로 */
|
||||
.ui-collapsible__title > .b01-dashboard__actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.b01-dashboard__company-pick .ui-field__label {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b01-dashboard__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -192,3 +192,44 @@
|
||||
| Q125 | 기계설비 9-2-1 | QC001535 | 바탕만들기 Shot Blast 자재 두 줄(steel shot 0.215 · ø1㎜ 기준 0.415)은 별개 재료인가 기준 차이인가? | 별개 재료 / 기준 차이(하나만 씀) | 설계자가 고름 | | sub2_2 |
|
||||
| Q126 | 기계설비 11-1-3 | 없음(로직 없음) | 나사식 재료량표에 구경 32가 없는데 구경 30 값을 쓰는 게 맞나? (원문 30이 32의 오기인가) | 30 값 사용 / 오기로 보고 32로 고침 | 32일 때 30 값 | | sub2_2 |
|
||||
| Q127 | 기계설비 13-2-4 | QC001638 GC000893 | 강판 전기아크용접(V형 13-2-4)에 합금강 재질 입력 · 할증을 넣나? (13-2-3에는 있음) | 넣음 / 넣지 않음 | 재질 입력 · 할증 없음 | | sub2_2 |
|
||||
|
||||
## 자재표에 없는 품목 — 사용자 결정 (34)
|
||||
|
||||
자재표에 대응 품목이 없는 재료 줄 · 검색어를 비워 억지 연결을 뗌(계산 = 단가 없음 · 까닭 「자재표에 없는 품목」 · 품목을 골라 넣으면 그 값). 품목을 새로 넣을지 · 비슷한 품목으로 이을지 답.
|
||||
|
||||
| 번호 | 로직 키 | 재료 이름 | 단위 | 원문 절 | 지금 상태 |
|
||||
|---|---|---|---|---|---|
|
||||
| N01 | GF000165 | 핀폴 손료 | 개 | 12-8 | 검색어 비움 |
|
||||
| N02 | GF000181 | 원형거푸집(PE 10회) 스라브 및 기초 · 벽체 | 조 | 12-16 | 검색어 비움(2줄) |
|
||||
| N03 | GF000202 | 탄성고무받침(150×150×15) | ㎡ | 12-30-2 | 검색어 비움 |
|
||||
| N04 | GC000348 | 일체식 방수시트 | ㎡ | 토목 3-4-1 | 검색어 비움 |
|
||||
| N05 | GC000432 | 메탈리밍쉘 NX | 개 | 토목 8-1-3 | 검색어 그대로 — BX 는 품목 있음 · NX 는 후보 없음(계산 = 단가 없음) |
|
||||
| N06 | GC000433 | 샘플러(표준관입) | 개 | 토목 8-2-1 | 검색어 비움 |
|
||||
| N07 | GC000435 | vane blade(대형) 베인 블레이드 | 개 | 토목 8-2-2 | 검색어 비움 |
|
||||
| N08 | GC000447 GC000448 GC000449 | 에어해머 | 개 | 토목 8-4-2 | 검색어 비움(3줄) |
|
||||
| N09 | GC000181 | P.P로프(9㎜) | m | 5-2-1 참고자료 | 검색어 비움 |
|
||||
| N10 | GC000197 | 지오콤포지트 6.0㎜ | ㎡ | 5-4-1 ⑧ | 검색어 비움 |
|
||||
| N11 | GC000212 | 강재거푸집 손료 | ㎡ | 6-3-2 | 검색어 비움 |
|
||||
| N12 | GC000216 | 원형맨홀거푸집 ø740~ø1800 | 조 | 6-3-5 | 검색어 비움(5줄) |
|
||||
| N13 | GC000027 | 손잡이기둥 | 개 | 2-7-6 참고자료 | 검색어 비움 |
|
||||
| N14 | GC000127 | 각재 10㎝×10㎝ | ㎥ | 3-8-3 ④ | 검색어 비움 |
|
||||
| N15 | GC000560 | 잠호용접와이어 | ㎏ | 1-1-3·1-1-4 | 검색어 비움 |
|
||||
| N16 | GC000576 | 치장벽돌 | 매 | 2-1-2 | 검색어 비움 |
|
||||
| N17 | GC000616 | 테라조 걸레받이 | m | 5-3-5 | 검색어 비움 |
|
||||
| N18 | GC000616 | 합성수지 걸레받이 | m | 5-3-5 | 검색어 비움 |
|
||||
| N19 | GC000646 | 조인트테이프 | m | 6-5-3 | 검색어 비움 |
|
||||
| N20 | GC000646 | 벤토나이트방수재 시트용 | ㎡ | 6-5-3 | 검색어 비움 |
|
||||
| N21 | GC000670 | AL리벳 | 개 | 8-2-2 | 검색어 비움 |
|
||||
| N22 | GC000723 | 동 및 동합금용 용접봉(BCuP3) | ㎏ | 1-2-1 참고자료 | 검색어 비움 |
|
||||
| N23 | GC000931 | 걸레 | ㎏ | 13-6-1 | 검색어 비움 |
|
||||
| N24 | GC000932 | 걸레 | ㎏ | 13-6-2 | 검색어 비움 |
|
||||
| N25 | GC000986 | 걸레 | ㎏ | 13-10-2 | 검색어 비움 |
|
||||
| N26 | GC000993 | 걸레 | ㎏ | 13-11-5 | 검색어 비움 |
|
||||
| N27 | GC000932 | 신주용접봉 | ㎏ | 13-6-2 | 검색어 비움 |
|
||||
| N28 | GC000972 | 가루비누 | ㎏ | 13-7-22 | 검색어 비움 |
|
||||
| N29 | GC000947 | 톱날 | 개 | 13-6-15 | 검색어 비움 |
|
||||
| N30 | GC000947 | Bite | 개 | 13-6-15 | 검색어 비움 |
|
||||
| N31 | GC000947 | Bite | 개 | 13-6-15 | 검색어 비움 |
|
||||
| N32 | GC000993 | 용접면 | 개 | 13-11-5 | 검색어 비움 |
|
||||
| N33 | GC000993 | 용접홀다 | 개 | 13-11-5 | 검색어 비움 |
|
||||
| N34 | GC000993 | 용접앞치마 | 개 | 13-11-5 | 검색어 비움 |
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# M01 원문 ↔ 자료 표 대조 보고 · sub1_3 (산림 1장 59표 + 2장 24표 + 3장 16표 = 99표)
|
||||
|
||||
점검만 · 코드 · 로직 · 자료 수정 없음 · 기록은 `tmp/M01_점검/sub1_3/` (진행.md · 보고_줄글.md · 이 파일 · 도구 `tools/`) · 방식 = `docs/raw/M01_점검/대조방식.md`.
|
||||
|
||||
## 1. 결과 한눈에
|
||||
|
||||
- 범위 99표 전부 판정(미판정 0). 표 키 99/99 가 자료 파일 셋(계수_산림품셈_01장 59 · 소요량_산림품셈_02장 24 · 소요량_산림품셈_03장 16)과 진행.md 에서 빠짐 · 겹침 없이 일치.
|
||||
- **표값(칸 값)**: 끝남 99 · 흠 0. 「끝남」 99줄 전부 근거 칸에 `N/N · 원문 파일 · 절` 있음(N 이 서로 같음 — 어긋난 칸 0). 원문의 숫자 · 단위 · 병합 칸 · 원문 표기(괄호 · 「6-7」 · 「〃」 등)가 자료와 같음.
|
||||
- **흠·의심 23줄**(확실 11 · 의심 12) — 표 칸 값의 흠은 0 · 흠은 표 형식(범위규칙 글 · 조건 겹침) 8표 · 연결 표시 2표 · 로직 쪽 13표(2장 소요재료를 로직이 안 씀 · 3-2 로직 없음 등).
|
||||
- **엔진 찾기 시험**: 99표 줄 전부를 찾기로 불러 봄 → 못 찾는 표 8(범위규칙 글 5 · 정수 조건 1 · 줄 겹침 2). 값 칸이 범위인 참고표 7(CF000004 · 008 · 010 · 012 · 014 · 017 · 059)은 「입력으로 받는 값」 이라 찾기 대상 아님.
|
||||
- **로직**: 맞음 22 · 흠/의심 15 · 미사용 35 · 해당 없음 27. 1장 1-4 할증표(CF000027~056)는 쓰는 로직이 거의 없음(CF000038 · 043 · 052 · 054 만) — 28-2a 새 로직 비고에 「할인·할증 못 넣음」 이라 적힌 것과 같은 이유(찾기 못하는 표가 일부).
|
||||
|
||||
| 부문·장 | 표 | 끝남 | 확실(표) | 의심(표) | 찾기 못함 | 로직 맞음 | 로직 흠·의심 | 미사용 | 해당 없음 |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 산림 1장 적용기준 | 59 | 59 | 5 | 3 | 7 | 4 | 0 | 33 | 22 |
|
||||
| 산림 2장 소요재료 및 기계손료 | 24 | 24 | 5 | 8 | 1 | 5 | 13 | 2 | 4 |
|
||||
| 산림 3장 작업장 관리 | 16 | 16 | 1 | 1 | 0 | 13 | 2 | 0 | 1 |
|
||||
| 합계 | 99 | 99 | 11 | 12 | 8 | 22 | 15 | 35 | 27 |
|
||||
|
||||
## 2. 대조 방법 (사실)
|
||||
|
||||
- **점검원**: 하위 점검원 없음 — sub1 이 99표를 혼자 처음부터 끝까지 대조(재확인은 아래 4장 · 같은 사람이 다른 길로 다시 돌림).
|
||||
- **원문**: `resources/knowledge/original/원가계산/산림_표준품셈/본문/` 제01장(1-01~1-07) · 제02장(2-01 · 2-02) · 제03장(3-01~3-08) md 열다섯 파일. 표마다 md 표와 그 표 앞뒤 [주] · 예시 글까지 같이 읽음.
|
||||
- **칸 대조(눈)**: `tools/side.py` 로 원문 md 와 자료 표 줄을 나란히 찍어 줄마다 · 칸마다 눈으로 맞댐 — 병합 칸(CF000025 · 031 · 043 · 046 · 057 · QF000027 · 032 등)은 위 줄에서 이어 읽어 자료가 복사한 값과 맞춤 · 큰 표는 줄 하나씩(CF000006 73줄 · CF000020 50줄 · QF000038 30칸 · CF000012 17줄).
|
||||
- **스크립트 보조 4가지**: `tools/numdiff.py`(절별 원문 표 수 ↔ 자료 표 수 다중집합 비교 — 어긋남은 부호 「−」 · 머리 글 수 · 주 글 속 수뿐 · 표 칸 수 어긋남 0) · `tools/notes.py`(자료 표 [주] 161줄이 원문에 그대로 있는지 — 없는 17줄은 병합 · 요약 문장이라 눈으로 수 · 뜻 확인, 값 다름 0) · `tools/lookup_try.py`(표 99개 줄 전부를 엔진 찾기로 불러 보기) · `tools/reread.py`(재확인 두 번째 길).
|
||||
- **로직 쪽**: 표를 쓰는 산림 로직 46개(GF000001~012 · 017 · 018 · 020 · 051 · 052 · 054~056 · 061~070 · 073~079 · 081 · 083 · 084 · 308 · 309 · 312~315)의 식에서 표를 부르는 줄을 읽고 표 · 원문과 맞춤(3장 GF000001~010 은 로직 전체) · GF000002(작업로 설치) 계산도 돌려 휘발유 · 잡품 · 오일 · 손료 금액을 손으로 맞춤.
|
||||
|
||||
## 3. 흠 · 의심 목록
|
||||
|
||||
한 줄 = 한 흠 · 자세한 문장은 `보고_줄글.md`. 판정: 표 칸 값이 원문과 다른 흠은 없음 · 아래는 표 형식 · 연결 표시 · 로직 쪽.
|
||||
|
||||
| 판정 | 키 | 이름 | 절 | 갈래 | 한 줄 |
|
||||
|---|---|---|---|---|---|
|
||||
| 확실 | CF000027 | 품의 할증 — 작업시기 | 1-4-1 | 기타(표 형식) | 범위규칙이 「작업시기(월) 이상·이하 — …」 글이라 엔진이 「모르는 범위규칙」 으로 멈춤 — 6~9월 등 월 구간을 찾기로 못 부름(값 3줄 자체는 원문과 같음) |
|
||||
| 확실 | CF000029 | 품의 할증 — 집단화정도 | 1-4-3 | 기타(표 형식) | 범위규칙이 「개소당 평균면적(ha) 이상·미만 — …」 글이라 「모르는 범위규칙」 으로 멈춤 — 1ha · 3ha 구간 찾기 불가(값 3줄은 원문과 같음) |
|
||||
| 확실 | CF000034 | 품의 할증 — 작업장까지의 이동거리 | 1-4-6 | 기타(표 형식) | 범위규칙이 「이동거리(㎞) 이상·미만 — …」 글이라 「모르는 범위규칙」 으로 멈춤 — 1.3㎞ · 2.5㎞ 구간 찾기 불가(값 6줄은 원문과 같음) |
|
||||
| 확실 | CF000044 | 품의 할증 — 횡단운반거리 | 1-4-15 | 기타(표 형식) | 범위규칙이 「횡단운반거리(m) 이상·이하 — …」 글이라 「모르는 범위규칙」 으로 멈춤 — 0~5 · 6~10 · 11~20m 구간 찾기 불가(값 3줄은 원문과 같음) |
|
||||
| 확실 | QF000009 | 소금처리(본당) | 2-1-5-2 | 기타(표 형식) | 범위규칙 「㎝미만·㎝이상 표기대로」 를 엔진이 모름 — 주두부직경 구간 찾기 불가(값 4줄은 원문과 같음) · 그래서 GF000304 도 못 씀 |
|
||||
| 확실 | QF000017 | 드론 무인 헬리콥터(ha당) | 2-1-9 | 로직 일부 구현 | 원문 표의 휘발유 1.13ℓ/ha · 잡품 주재료비의 45% 가 있는데 GF000320(8-6-2)에 연료 줄이 없음 — 비고에 「연료 원문 표에 없음」 이라 적었으나 2-1-9 에 있음(휘발유 단가는 자료에 있음) |
|
||||
| 확실 | QF000019 | 드론 무인 멀티콥터(ha당) | 2-1-10 | 로직 일부 구현 | 원문 표의 휘발유(발전기) 0.81ℓ/ha · 잡품 30% 가 있는데 GF000320 멀티콥터 쪽에 연료 줄이 없음 |
|
||||
| 확실 | QF000021 | 지상 약제살포 | 2-1-11 | 로직 일부 구현 | 원문 표의 경유(차량살포) 1.3ℓ/ha(15ha 20ℓ) · 잡품 5% 가 있는데 GF000321(8-6-3)에 연료 줄이 없음(GF000321 은 원/일 — ha 환산 필요) |
|
||||
| 확실 | QF000026 | 작업로 예정선 선정 및 표식 | 3-2 | 로직 없음(절 누락) | 초급기술자 1.0인/1일 1km 표가 있는데 3-2 로직이 없음(3장 로직은 GF000001~010 — 3-2 없음) · 초급기술자 노임 LB000293 은 인력에 있음 · 표 값은 원문과 같음 |
|
||||
| 확실 | CF000043 | 품의 할증 — 집재방향 | 1-4-14 | 기타(연결 표시) | 표의 로직 목록에 실제로 쓰는 GF000309 · GF000312 · GF000313(28-2a 새 로직)이 빠짐 |
|
||||
| 확실 | QF000002 | 경유 사용 임업기계 연료 | 2-1-2 | 기타(연결 표시) | 표의 로직 목록에 실제로 쓰는 GF000308 · 309 · 312 · 313 · 314 · 315(28-2a 새 로직)가 빠짐 |
|
||||
| 의심 | CF000055 | 품의 할증 — 작업시간 제한 | 1-4-25 | 기타(표 형식) | 원문은 「2시간 이하 … 6시간 이하」 구간인데 조건이 「수」 정확값 — 2.5시간 · 4.5시간을 넣으면 「줄이 없음」(엔진 「이하」 범위규칙이면 해결) |
|
||||
| 의심 | CF000006 | 설계서의 단위 및 소수의 표준 | 1-2-2 | 기타(표 형식) | 조건 칸이 종목 하나뿐이라 같은 종목 여러 줄(견치돌 · 돌쌓기 · 목재 · 말뚝 · 돌망태 · 관류 16줄)이 겹쳐 찾기가 「줄이 2개」 로 멈춤 — 참고표라 쓰는 로직 없음 |
|
||||
| 의심 | CF000020 | 화물자동차의 적재량 | 1-2-7 | 기타(표 형식) | 조건 칸(종별 · 규격)만으로 안 갈리는 줄(블록 · 페인트 · 흄관 · 콘크리트관 · 주철관 · 도복장강관 · P·C 파일 · 전주 12줄)이 겹쳐 「줄이 2개」 로 멈춤 — 쓰는 로직 없음 |
|
||||
| 의심 | QF000008 | 덩굴 피복도별 농약 소요량 증감률 | 2-1-5-1 | 로직 일부 구현 | 원문 예시 두 줄(밀 +20 · 과소 −50)만 수록 — 원문 주②는 덩굴 피복도(1-4-11 5등급: 과밀 +50 · 소 −20 포함)에 따라 가감이라 GF000055 가 과밀 · 소를 못 씀 |
|
||||
| 의심 | QF000028 | 임산물 운반로 신설비 산정(예시) | 3-4-2 | 계산식 어긋남 | GF000003 이 Q 를 3자리(8.946 · 공통 8-1-4 「소수점이하 3자리」 규칙)로 반올림하나 산림 원문 예시는 8.95(2자리) — 결과 0.05% 차이 · 어느 쪽 따를지 사람 확인 |
|
||||
| 의심 | QF000005 | 페인트 및 마킹테이프 재료비 | 2-1-4 | 로직 일부 구현 | 경계표시(3-1) 페인트 0.2ℓ/ha · 벌도 위험목 점검(4-5) 0.1ℓ/ha · 작업로 선정 페인트 · 마킹테이프가 있는데 GF000001 · GF000014 에 재료 줄이 없고 3-2 는 로직 없음(수성페인트는 자재품목에 있음 · 마킹테이프는 없음) |
|
||||
| 의심 | QF000010 | 소금처리 잡품율 | 2-1-5-2 | 로직 일부 구현 | GF000304(6-4-3)에 소금(본당 g) · 잡품 10% 줄이 없음 — 소금은 자재품목에 없음(비고에 「소금 원문 표에 없음」 이라 적었으나 2-1-5-2 에 있음) |
|
||||
| 의심 | QF000011 | 뿌리제거(100본당) | 2-1-5-3 | 로직 일부 구현 | GF000305(6-4-4)에 농약(글리포세이트 1.5병/100본) · 친환경 비닐랩 300매/100본 · 잡품 66% 줄이 없음(글리포세이트 · 비닐랩은 자재품목에 없음) |
|
||||
| 의심 | QF000012 | 나무주사(100본당) | 2-1-6 | 로직 일부 구현 | GF000317 · 318(8-1)에 천공테이프 · 페인트 · 표식라벨 · 천공기날 등 재료 줄이 없음(라벨 · 천공기날은 자재품목에 없음) — 2-1-1 천공기 휘발유 3ℓ/일 · 잡품 95% · 손료(EQ000643)도 안 씀 |
|
||||
| 의심 | QF000014 | 소각, 매몰, 훈증, 박피 | 2-1-7 | 로직 일부 구현 | 훈증약제 · 피복제 · 표식라벨 · 벌근훈증약제 5ℓ · 벌근피복제 100개 · 약종별 약제량이 GF000078 · 079(8-3) · GF000083(8-8-1)에 없음(메탐소듐 액제 · 디메틸디설파이드는 자재품목에 없음) |
|
||||
| 의심 | QF000023 | 파쇄 소모품 | 2-1-13 | 로직 일부 구현 | 메인파쇄기날 0.00125개/hr · 분쇄기날 0.005개/hr · 잡품 5% 가 GF000323(8-11)에 없음(파쇄기날 자재품목에 없음) |
|
||||
| 의심 | QF000003 | 우드그래플 소모성 경비 | 2-1-3 | 로직 일부 구현 | 원문은 「산지에서 우드그랩이 이용되는 품에 적용」 인데 GF000069 만 소모성경비를 더하고 같은 우드그래플 품 GF000070 · 314 · 315 는 안 더함 |
|
||||
|
||||
## 4. 재확인 (10표에 1표)
|
||||
|
||||
재확인 10표 = CF000006 · 012 · 020 · 025 · 034 · 046 · 057 · QF000002 · 032 · 038. 원문 md 표를 줄 단위로 읽어 병합 칸을 채운 뒤 자료 표 줄과 「줄마다 수의 차례」 를 맞댐(`tools/reread.py`).
|
||||
|
||||
| 표 | 원문 줄 | 자료 줄 | 어긋난 줄 | 비고 |
|
||||
|---|---:|---:|---:|---|
|
||||
| CF000006 | 73 | 73 | 0 | |
|
||||
| CF000012 | 17 | 17 | 0 | |
|
||||
| CF000020 | 50 | 50 | 0 | 시멘트 줄 1곳은 원문 칸 「25.5톤 화물차는 풀카고기준」 이 자료 「20톤 트레일러원문」 칸에 있어 수 차례에서 빠질 뿐(뜻 같음) |
|
||||
| CF000025 | 17 | 17 | 0 | |
|
||||
| CF000034 | 6 | 6 | 0 | |
|
||||
| CF000046 | 9 | 9 | 0 | |
|
||||
| CF000057 | 21(빈 줄 5 포함) | 16 | — | 원문 서식에 값 없는 줄(간접재료비 · 소계 · 총원가)이 있어 자동 맞댐이 안 됨 → 눈으로 16줄 재대조 · 어긋남 0 |
|
||||
| QF000002 | 12 | 12 | 0 | |
|
||||
| QF000032 | 9 | 9 | 0 | 소요인력 값 9개 같음 · 병합 칸(인력구분 50% · 50%)을 자료가 아홉 줄에 복사한 것은 정상 |
|
||||
| QF000038 | 30칸 | 30 | 0 | |
|
||||
|
||||
첫 대조(눈)와 두 번째 길(스크립트)이 모두 같음 — 재확인으로 뒤집힌 판정 0.
|
||||
|
||||
## 5. 로직 쪽 참고
|
||||
|
||||
- **맞음 22표**: CF000038 · CF000043 · CF000052 · CF000054 · QF000001 · QF000002 · QF000004 · QF000007 · QF000016 · QF000025 · QF000027 · QF000029 · QF000030 · QF000031 · QF000032 · QF000033 · QF000034 · QF000035 · QF000036 · QF000037 · QF000038 · QF000039.
|
||||
- **미사용 35표**: CF000012 · CF000015 · CF000016 · CF000020 · CF000021 · CF000022 · CF000023 · CF000024 · CF000025 · CF000026 · CF000027 · CF000028 · CF000029 · CF000031 · CF000033 · CF000034 · CF000035 · CF000036 · CF000037 · CF000039 · CF000040 · CF000042 · CF000044 · CF000045 · CF000046 · CF000047 · CF000048 · CF000049 · CF000051 · CF000053 · CF000055 · CF000056 · CF000059 · QF000015 · QF000024 — 값 표인데 쓰는 로직이 없음(1-4 할증표 · 재료할증 · 적재량 · 체적변화율 · 공제율 · 공사용수 · 2장 재료표 대부분 — 설계상 못 넣었거나 아직 안 넣음).
|
||||
- **해당 없음 27표**: 정의 · 분류 · 표시 규칙 · 원가 서식 등 계산식 재료가 아닌 참고표.
|
||||
|
||||
## 6. 범위 밖 참고 · 정정
|
||||
|
||||
- 2-2 기계손료 11절(체인톱 · 예취기 · 집재장비 20종 · 배부식분무기 · 천공기 · 양수기 · 드론 · 지상 약제살포 · 파쇄 · 기타장비)은 소요량 파일에 표가 없고 `기계.json`(EQ000620~654)에 있음. 손료계수 29 · 시간당 손료계수 1 · 장비가격 20 · 취득가액 3(53칸) 전부 원문과 같음을 확인 — 이 표 99개 밖이라 판정 줄에는 안 올림.
|
||||
- 자료 표 [주] 문구 경미 차이 2건(값 영향 없음 · 흠 아님): QF000030 첫 [주] 「완전, 복구 경비」→「완전복구 경비」 · 둘째 [주] 「다음 조건에 따라」→「위 조건에 따라」.
|
||||
- **정정(sub1 본인 기록)**: 28-2a 새 로직 비고 중 「재료 원문 표에 없음」 · 「연료 원문 표에 없음」 이라 적은 GF000304 · 305 · 317 · 318 · 320 · 321 · 323 은 2장 소요재료에 수량표가 있음(위 줄글). 자료는 이번에 안 고침.
|
||||
|
||||
## 7. 사람 확인 · 남은 것
|
||||
|
||||
- QF000028 반올림 자리(공통 8-1-4 3자리 ↔ 산림 예시 2자리) · QF000008 과밀 · 소 등급을 어디에 둘지(2-1-5-1 표 늘리기 ↔ 1-4-11 을 로직이 직접 부르기) · 3-2 작업로 선정 로직을 새로 만들지.
|
||||
- 범위규칙 글 표 5(CF000027 · 029 · 034 · 044 · QF000009) · 정수 조건 CF000055 는 표 고침(28-2d 계열) 뒤 로직을 넣을 수 있음.
|
||||
- 2장 재료 줄(위 QF 흠 12줄)은 자재품목에 없는 품목(소금 · 마킹테이프 · 글리포세이트 · 비닐랩 · 표식라벨 · 천공기날 · 메탐소듐 · 파쇄기날)은 자재 자료를 넣거나 빈 줄로 남기는 결정이 필요.
|
||||
@@ -0,0 +1,27 @@
|
||||
# M01 점검 결과 · 줄글 · sub1_3 (산림 1~3장)
|
||||
|
||||
흠·의심 하나에 한 줄: `키 | 이름 | 절 | 갈래 | 설명 | 확실/의심`. 표 값(칸) 흠은 0 — 아래는 표 형식 · 연결 표시 · 로직 쪽. 점검만 · 자료 안 고침.
|
||||
|
||||
CF000027 | 품의 할증 — 작업시기 | 1-4-1 | 기타(표 형식) | 범위규칙이 「작업시기(월) 이상·이하 — …」 글이라 엔진이 「모르는 범위규칙」 으로 멈춤 — 6~9월 등 월 구간을 찾기로 못 부름(값 3줄 자체는 원문과 같음) | 확실
|
||||
CF000029 | 품의 할증 — 집단화정도 | 1-4-3 | 기타(표 형식) | 범위규칙이 「개소당 평균면적(ha) 이상·미만 — …」 글이라 「모르는 범위규칙」 으로 멈춤 — 1ha · 3ha 구간 찾기 불가(값 3줄은 원문과 같음) | 확실
|
||||
CF000034 | 품의 할증 — 작업장까지의 이동거리 | 1-4-6 | 기타(표 형식) | 범위규칙이 「이동거리(㎞) 이상·미만 — …」 글이라 「모르는 범위규칙」 으로 멈춤 — 1.3㎞ · 2.5㎞ 구간 찾기 불가(값 6줄은 원문과 같음) | 확실
|
||||
CF000044 | 품의 할증 — 횡단운반거리 | 1-4-15 | 기타(표 형식) | 범위규칙이 「횡단운반거리(m) 이상·이하 — …」 글이라 「모르는 범위규칙」 으로 멈춤 — 0~5 · 6~10 · 11~20m 구간 찾기 불가(값 3줄은 원문과 같음) | 확실
|
||||
QF000009 | 소금처리(본당) | 2-1-5-2 | 기타(표 형식) | 범위규칙 「㎝미만·㎝이상 표기대로」 를 엔진이 모름 — 주두부직경 구간 찾기 불가(값 4줄은 원문과 같음) · 그래서 GF000304 도 못 씀 | 확실
|
||||
QF000017 | 드론 무인 헬리콥터(ha당) | 2-1-9 | 로직 일부 구현 | 원문 표의 휘발유 1.13ℓ/ha · 잡품 주재료비의 45% 가 있는데 GF000320(8-6-2)에 연료 줄이 없음 — 비고에 「연료 원문 표에 없음」 이라 적었으나 2-1-9 에 있음(휘발유 단가는 자료에 있음) | 확실
|
||||
QF000019 | 드론 무인 멀티콥터(ha당) | 2-1-10 | 로직 일부 구현 | 원문 표의 휘발유(발전기) 0.81ℓ/ha · 잡품 30% 가 있는데 GF000320 멀티콥터 쪽에 연료 줄이 없음 | 확실
|
||||
QF000021 | 지상 약제살포 | 2-1-11 | 로직 일부 구현 | 원문 표의 경유(차량살포) 1.3ℓ/ha(15ha 20ℓ) · 잡품 5% 가 있는데 GF000321(8-6-3)에 연료 줄이 없음(GF000321 은 원/일 — ha 환산 필요) | 확실
|
||||
QF000026 | 작업로 예정선 선정 및 표식 | 3-2 | 로직 없음(절 누락) | 초급기술자 1.0인/1일 1km 표가 있는데 3-2 로직이 없음(3장 로직은 GF000001~010 — 3-2 없음) · 초급기술자 노임 LB000293 은 인력에 있음 · 표 값은 원문과 같음 | 확실
|
||||
CF000043 | 품의 할증 — 집재방향 | 1-4-14 | 기타(연결 표시) | 표의 로직 목록에 실제로 쓰는 GF000309 · GF000312 · GF000313(28-2a 새 로직)이 빠짐 | 확실
|
||||
QF000002 | 경유 사용 임업기계 연료 | 2-1-2 | 기타(연결 표시) | 표의 로직 목록에 실제로 쓰는 GF000308 · 309 · 312 · 313 · 314 · 315(28-2a 새 로직)가 빠짐 | 확실
|
||||
CF000055 | 품의 할증 — 작업시간 제한 | 1-4-25 | 기타(표 형식) | 원문은 「2시간 이하 … 6시간 이하」 구간인데 조건이 「수」 정확값 — 2.5시간 · 4.5시간을 넣으면 「줄이 없음」(엔진 「이하」 범위규칙이면 해결) | 의심
|
||||
CF000006 | 설계서의 단위 및 소수의 표준 | 1-2-2 | 기타(표 형식) | 조건 칸이 종목 하나뿐이라 같은 종목 여러 줄(견치돌 · 돌쌓기 · 목재 · 말뚝 · 돌망태 · 관류 16줄)이 겹쳐 찾기가 「줄이 2개」 로 멈춤 — 참고표라 쓰는 로직 없음 | 의심
|
||||
CF000020 | 화물자동차의 적재량 | 1-2-7 | 기타(표 형식) | 조건 칸(종별 · 규격)만으로 안 갈리는 줄(블록 · 페인트 · 흄관 · 콘크리트관 · 주철관 · 도복장강관 · P·C 파일 · 전주 12줄)이 겹쳐 「줄이 2개」 로 멈춤 — 쓰는 로직 없음 | 의심
|
||||
QF000008 | 덩굴 피복도별 농약 소요량 증감률 | 2-1-5-1 | 로직 일부 구현 | 원문 예시 두 줄(밀 +20 · 과소 −50)만 수록 — 원문 주②는 덩굴 피복도(1-4-11 5등급: 과밀 +50 · 소 −20 포함)에 따라 가감이라 GF000055 가 과밀 · 소를 못 씀 | 의심
|
||||
QF000028 | 임산물 운반로 신설비 산정(예시) | 3-4-2 | 계산식 어긋남 | GF000003 이 Q 를 3자리(8.946 · 공통 8-1-4 「소수점이하 3자리」 규칙)로 반올림하나 산림 원문 예시는 8.95(2자리) — 결과 0.05% 차이 · 어느 쪽 따를지 사람 확인 | 의심
|
||||
QF000005 | 페인트 및 마킹테이프 재료비 | 2-1-4 | 로직 일부 구현 | 경계표시(3-1) 페인트 0.2ℓ/ha · 벌도 위험목 점검(4-5) 0.1ℓ/ha · 작업로 선정 페인트 · 마킹테이프가 있는데 GF000001 · GF000014 에 재료 줄이 없고 3-2 는 로직 없음(수성페인트는 자재품목에 있음 · 마킹테이프는 없음) | 의심
|
||||
QF000010 | 소금처리 잡품율 | 2-1-5-2 | 로직 일부 구현 | GF000304(6-4-3)에 소금(본당 g) · 잡품 10% 줄이 없음 — 소금은 자재품목에 없음(비고에 「소금 원문 표에 없음」 이라 적었으나 2-1-5-2 에 있음) | 의심
|
||||
QF000011 | 뿌리제거(100본당) | 2-1-5-3 | 로직 일부 구현 | GF000305(6-4-4)에 농약(글리포세이트 1.5병/100본) · 친환경 비닐랩 300매/100본 · 잡품 66% 줄이 없음(글리포세이트 · 비닐랩은 자재품목에 없음) | 의심
|
||||
QF000012 | 나무주사(100본당) | 2-1-6 | 로직 일부 구현 | GF000317 · 318(8-1)에 천공테이프 · 페인트 · 표식라벨 · 천공기날 등 재료 줄이 없음(라벨 · 천공기날은 자재품목에 없음) — 2-1-1 천공기 휘발유 3ℓ/일 · 잡품 95% · 손료(EQ000643)도 안 씀 | 의심
|
||||
QF000014 | 소각, 매몰, 훈증, 박피 | 2-1-7 | 로직 일부 구현 | 훈증약제 · 피복제 · 표식라벨 · 벌근훈증약제 5ℓ · 벌근피복제 100개 · 약종별 약제량이 GF000078 · 079(8-3) · GF000083(8-8-1)에 없음(메탐소듐 액제 · 디메틸디설파이드는 자재품목에 없음) | 의심
|
||||
QF000023 | 파쇄 소모품 | 2-1-13 | 로직 일부 구현 | 메인파쇄기날 0.00125개/hr · 분쇄기날 0.005개/hr · 잡품 5% 가 GF000323(8-11)에 없음(파쇄기날 자재품목에 없음) | 의심
|
||||
QF000003 | 우드그래플 소모성 경비 | 2-1-3 | 로직 일부 구현 | 원문은 「산지에서 우드그랩이 이용되는 품에 적용」 인데 GF000069 만 소모성경비를 더하고 같은 우드그래플 품 GF000070 · 314 · 315 는 안 더함 | 의심
|
||||
@@ -0,0 +1,126 @@
|
||||
# M01 표 행 대조 진행 · sub1_3 (산림 1장 59표 + 2장 24표 + 3장 16표 = 99표)
|
||||
|
||||
판정 말 — 끝남(흠 0) · 확실(흠) · 의심. 표값 = 원문 표와 자료 표 칸 대조(N/N = 자료 칸 수 · 원문에서 짝을 찾은 칸 수 · 어긋남 0). 찾기 = 엔진 찾기 시험(줄 전부를 찾기로 불러 봄). 로직 = 로직이 그 표를 쓰는 방식(따로 판정). 재확인 = 두 번째 길(줄 단위 수 차례 맞대기)을 다시 돌린 표.
|
||||
|
||||
| 부문·장 | 표 | 끝남 | 확실(흠) | 의심 | 찾기 못함 | 로직 맞음 | 로직 흠·의심 | 미사용 | 해당 없음 | 재확인 |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 산림 1장 적용기준 | 59 | 59 | 5 | 3 | 7 | 4 | 0 | 33 | 22 | 7 |
|
||||
| 산림 2장 소요재료 및 기계손료 | 24 | 24 | 5 | 8 | 1 | 5 | 13 | 2 | 4 | 1 |
|
||||
| 산림 3장 작업장 관리 | 16 | 16 | 1 | 1 | 0 | 13 | 2 | 0 | 1 | 2 |
|
||||
| 합계 | 99 | 99 | 11 | 12 | 8 | 22 | 15 | 35 | 27 | 10 |
|
||||
|
||||
「확실 · 의심」 열 = 아래 흠 줄글(보고_줄글.md)에 오른 표 수. 표값(칸 값) 흠은 0 — 흠은 모두 표 형식(찾기) · 연결 표시 · 로직 쪽.
|
||||
|
||||
## 산림 1장 적용기준
|
||||
|
||||
| 표 | 절 | 이름 | 표값 | 표값 근거 | 찾기 | 로직 | 로직 근거 | 흠·의심(줄글) | 재확인 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| CF000001 | 1-1-3 | 적용방법 — 품의 일일 작업시간 기준 | 끝남 | 1/1 · 1-01_일반사항.md · 1-1-3 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000002 | 1-2-1 | 수량의 계산 — 소수 처리 · 유효숫자 | 끝남 | 8/8 · 1-02_설계_및_수량.md · 1-2-1 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000003 | 1-2-1 | 총축적 계산 — 측정 기준 | 끝남 | 4/4 · 1-02_설계_및_수량.md · 1-2-1 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000004 | 1-2-1 | 가슴높이지름 2㎝ 괄약 — 대표값별 범위 | 끝남 | 4/4 · 1-02_설계_및_수량.md · 1-2-1 | 값 칸이 범위(참고표) | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000005 | 1-2-1 | 수량 공제 제외 — 포장공종 1개소당 구조물 자리 | 끝남 | 1/1 · 1-02_설계_및_수량.md · 1-2-1 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000006 | 1-2-2 | 설계서의 단위 및 소수의 표준 | 끝남 | 231/231 · 1-02_설계_및_수량.md · 1-2-2 · 원문 73줄 전부 · 종목 병합 칸 채움 · 짝(가·나·다·라)은 병합 아래 줄 표시 | 16줄 겹침(줄이 2개) | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | 의심 · 기타(표 형식) | 예 |
|
||||
| CF000007 | 1-2-2 | 금액의 단위 표준 | 끝남 | 20/20 · 1-02_설계_및_수량.md · 1-2-2 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000008 | 1-2-3 | 토질 및 암의 분류 — 암 종류별 균열간격 | 끝남 | 12/12 · 1-02_설계_및_수량.md · 1-2-3 | 값 칸이 범위(참고표) | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000009 | 1-2-3 | 돌재료 분류 — 견치돌 · 깬돌 · 깬잡석 형상 비율 | 끝남 | 8/8 · 1-02_설계_및_수량.md · 1-2-3 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000010 | 1-2-3 | 돌재료 분류 — 크기 기준 | 끝남 | 35/35 · 1-02_설계_및_수량.md · 1-2-3 | 값 칸이 범위(참고표) | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000011 | 1-2-3 | 체적의 변화 — 흐트러진상태 L · 다져진상태 C | 끝남 | 4/4 · 1-02_설계_및_수량.md · 1-2-3 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000012 | 1-2-3 | 체적의 변화율 L · C | 끝남 | 51/51 · 1-02_설계_및_수량.md · 1-2-3 · 원문 17줄 전부 · 물결표 「∼」 「~」 섞임은 같은 범위로 읽음 · 폐콘크리트 C 「별도 설계」 글 유지 | 값 칸이 범위(참고표) | 미사용 | 쓰는 로직 없음 | | 예 |
|
||||
| CF000013 | 1-2-3 | 체적환산계수(f) — 구하는 Q ÷ 기준이 되는 q | 끝남 | 8/8 · 1-02_설계_및_수량.md · 1-2-3 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000014 | 1-2-3 | 토취장 및 골재원 — 규격품 채취 시 사용 못 하는 파 | 끝남 | 1/1 · 1-02_설계_및_수량.md · 1-2-3 | 값 칸이 범위(참고표) | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000015 | 1-2-3 | 사토장 관리 — 토공운반 Q값 반영 시 보통인부 노임 | 끝남 | 1/1 · 1-02_설계_및_수량.md · 1-2-3 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000016 | 1-2-4 | 사용 고재 등 발생재 공제율 | 끝남 | 6/6 · 1-02_설계_및_수량.md · 1-2-4 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000017 | 1-2-6 | 공구손료 및 잡재료 — 명시 없는 잡재료 별도 계상 한 | 끝남 | 1/1 · 1-02_설계_및_수량.md · 1-2-6 | 값 칸이 범위(참고표) | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000018 | 1-2-7 | 운반 — 품에 포함된 소운반 거리 | 끝남 | 1/1 · 1-02_설계_및_수량.md · 1-2-7 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000019 | 1-2-7 | 운반 — 경사면 소운반 거리(직고 대 수평거리) | 끝남 | 2/2 · 1-02_설계_및_수량.md · 1-2-7 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000020 | 1-2-7 | 화물자동차의 적재량(일반 기준) | 끝남 | 286/286 · 1-02_설계_및_수량.md · 1-2-7 · 원문 50줄 전부 · 괄호 (350) (460) (300)(400)(550) 원문 표기 유지 · 시멘트 20톤 637 옆 「25.5톤 화물차는 풀카고기준」 은 원문 칸에 보관 | 12줄 겹침(줄이 2개) | 미사용 | 쓰는 로직 없음 | 의심 · 기타(표 형식) | 예 |
|
||||
| CF000021 | 1-3-1 | 재료 할증 — 콘크리트 및 포장용 재료 | 끝남 | 17/17 · 1-03_재료_및_노임의_할증.md · 1-3-1 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000022 | 1-3-1 | 재료 할증 — 노상 및 노반재료(선택층, 보조기층, 기 | 끝남 | 8/8 · 1-03_재료_및_노임의_할증.md · 1-3-1 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000023 | 1-3-1 | 재료 할증 — 관류 및 구조물 기초 부설재료 | 끝남 | 2/2 · 1-03_재료_및_노임의_할증.md · 1-3-1 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000024 | 1-3-1 | 재료 할증 — 강재류 | 끝남 | 22/22 · 1-03_재료_및_노임의_할증.md · 1-3-1 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000025 | 1-3-1 | 재료 할증 — 기타재료 | 끝남 | 44/44 · 1-03_재료_및_노임의_할증.md · 1-3-1 · 원문 17줄 · 목재 · 합판 · 레디믹스트 · 현장콘크리트 병합 칸 채움 | 됨 | 미사용 | 쓰는 로직 없음 | | 예 |
|
||||
| CF000026 | 1-3-2 | 노임 할증 — 작업반장 배치(산지사방, 야계사방, 해안 | 끝남 | 2/2 · 1-03_재료_및_노임의_할증.md · 1-3-2 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000027 | 1-4-1 | 품의 할증 — 작업시기 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-1 | 못 찾음 — 모르는 범위규칙 | 미사용 | 쓰는 로직 없음 — 범위규칙 때문에 못 씀 | 확실 · 기타(표 형식) | |
|
||||
| CF000028 | 1-4-2 | 품의 할증 — 조림 후 경과연수 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-2 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000029 | 1-4-3 | 품의 할증 — 집단화정도 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-3 | 못 찾음 — 모르는 범위규칙 | 미사용 | 쓰는 로직 없음 | 확실 · 기타(표 형식) | |
|
||||
| CF000030 | 1-4-3 | 집단화정도 — 사업지 간격 동일 개소 판정 | 끝남 | 1/1 · 1-04_품의_할인․할증.md · 1-4-3 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000031 | 1-4-4 | 품의 할인·할증 — 작업구역 | 끝남 | 21/21 · 1-04_품의_할인․할증.md · 1-4-4 · 원문 7줄 · 작업구역 4줄 + 벌채구역 크기 3줄 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000032 | 1-4-4 | 작업구역 — 개소 간 이격거리 동일 작업구역 판정 | 끝남 | 1/1 · 1-04_품의_할인․할증.md · 1-4-4 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000033 | 1-4-5 | 품의 할인·할증 — 경사도 | 끝남 | 21/21 · 1-04_품의_할인․할증.md · 1-4-5 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000034 | 1-4-6 | 품의 할증 — 작업장까지의 이동거리 | 끝남 | 18/18 · 1-04_품의_할인․할증.md · 1-4-6 | 못 찾음 — 모르는 범위규칙 | 미사용 | 쓰는 로직 없음 | 확실 · 기타(표 형식) | 예 |
|
||||
| CF000035 | 1-4-7 | 품의 할증 — 하층식생 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-7 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000036 | 1-4-8 | 품의 할증 — 장애물의 정도 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-8 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000037 | 1-4-9 | 품의 할증 — 제거대상 식생 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-9 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000038 | 1-4-10 | 품의 할증 — 토양상태 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-10 | 됨 | 맞음 | GF000017 · 018 · 020 토양조건 할증 3줄 = 표 그대로 | | |
|
||||
| CF000039 | 1-4-11 | 품의 할인·할증 — 덩굴피복도 | 끝남 | 10/10 · 1-04_품의_할인․할증.md · 1-4-11 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000040 | 1-4-12 | 품의 할인·할증 — 제거대상 피복도 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-12 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000041 | 1-4-12 | 제거대상 피복도 — 제거대상목 기준(조림목 수고 대비) | 끝남 | 1/1 · 1-04_품의_할인․할증.md · 1-4-12 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000042 | 1-4-13 | 품의 할증 — 주행 장애물 상태 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-13 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000043 | 1-4-14 | 품의 할증 — 집재방향 | 끝남 | 24/24 · 1-04_품의_할인․할증.md · 1-4-14 · 원문 7줄 · 병합 칸(집재방향 · 경사) 채움 | 됨 | 맞음 | GF000009 · 061 · 063 · 066 · 067 · 068 · 083 · 309 · 312 · 313 집재방향 할증(구분 · 방향 · 경사 조건 맞음) | 확실 · 기타(연결 표시) | |
|
||||
| CF000044 | 1-4-15 | 품의 할증 — 횡단운반거리 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-15 | 못 찾음 — 모르는 범위규칙 | 미사용 | 쓰는 로직 없음 | 확실 · 기타(표 형식) | |
|
||||
| CF000045 | 1-4-16 | 품의 할증 — 규격재 생산 | 끝남 | 2/2 · 1-04_품의_할인․할증.md · 1-4-16 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000046 | 1-4-17 | 품의 할인·할증 — 방제대상목 분포 | 끝남 | 27/27 · 1-04_품의_할인․할증.md · 1-4-17 · 원문 9줄 · 병해충 4 + 재선충 5 | 됨 | 미사용 | 쓰는 로직 없음 | | 예 |
|
||||
| CF000047 | 1-4-18 | 품의 할인·할증 — 방제대상목 평균 경급 | 끝남 | 15/15 · 1-04_품의_할인․할증.md · 1-4-18 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000048 | 1-4-19 | 품의 할증 — 매개충나무주사 | 끝남 | 3/3 · 1-04_품의_할인․할증.md · 1-4-19 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000049 | 1-4-20 | 품의 할인·할증 — 방제지 사면 | 끝남 | 12/12 · 1-04_품의_할인․할증.md · 1-4-20 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000050 | 1-4-20 | 방제지 사면 — 산록에서 산정부 방향 복합사면 할증 기 | 끝남 | 1/1 · 1-04_품의_할인․할증.md · 1-4-20 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000051 | 1-4-21 | 품의 할증 — 방제지 접근성 | 끝남 | 9/9 · 1-04_품의_할인․할증.md · 1-4-21 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000052 | 1-4-22 | 품의 할증 — 방제 수종 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-22 | 됨 | 맞음 | GF000076 · 077 · 078 · 079 방제수종 할증 | | |
|
||||
| CF000053 | 1-4-23 | 품의 할증 — 방제목 운반거리 | 끝남 | 18/18 · 1-04_품의_할인․할증.md · 1-4-23 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000054 | 1-4-24 | 품의 할인 — 방제장비 규격 | 끝남 | 7/7 · 1-04_품의_할인․할증.md · 1-4-24 | 됨 | 맞음 | GF000084 굴착기 0.2㎥ 0 · 0.4㎥ 이상 −20 | | |
|
||||
| CF000055 | 1-4-25 | 품의 할증 — 작업시간 제한 | 끝남 | 10/10 · 1-04_품의_할인․할증.md · 1-4-25 | 정수 시간만 찾음(2.5시간 못 찾음) | 미사용 | 쓰는 로직 없음 | 의심 · 기타(표 형식) | |
|
||||
| CF000056 | 1-4-26 | 품의 할증 — 소규모 작업물량 제한 | 끝남 | 6/6 · 1-04_품의_할인․할증.md · 1-4-26 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| CF000057 | 1-6 | 원가 구성 비목별 적용기준 | 끝남 | 70/70 · 1-06_원가_작성기준.md · 1-6 · 원문 서식 21줄 중 값 없는 빈 줄(간접재료비 · 소계 · 총원가) 빼고 16줄 · 병합 적용기준 칸 채움 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | 예 |
|
||||
| CF000058 | 1-7-1 | 거푸집 사용횟수 | 끝남 | 8/8 · 1-07_기타사항.md · 1-7-1 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| CF000059 | 1-7-4 | 공사용수 | 끝남 | 18/18 · 1-07_기타사항.md · 1-7-4 | 값 칸이 범위(참고표) | 미사용 | 쓰는 로직 없음 | | |
|
||||
|
||||
## 산림 2장 소요재료 및 기계손료
|
||||
|
||||
| 표 | 절 | 이름 | 표값 | 표값 근거 | 찾기 | 로직 | 로직 근거 | 흠·의심(줄글) | 재확인 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| QF000001 | 2-1-1 | 체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료 | 끝남 | 34/34 · 2-01_소요재료.md · 2-1-1 · 원문 7줄(체인톱 3 + 예취기 · 아키아윈치 · 2드럼 · 천공기) · 적용기준 두 줄 합침(천공기) | 됨 | 맞음 | GF000002 · 051 · 052 · 054 · 056 · 062 · 063 · 064 · 076 연료 · 잡품 · 오일(계산 시험 GF000002: 8.4ℓ · 잡품 = 휘발유의 40%) | | |
|
||||
| QF000002 | 2-1-2 | 경유 사용 임업기계 연료 | 끝남 | 39/39 · 2-01_소요재료.md · 2-1-2 | 됨 | 맞음 | GF000011 · 012 · 065 · 066 · 067 · 068 · 069 · 070 · 073 · 074 · 075 · 308 · 309 · 312 · 313 · 314 · 315 기계명 조건 맞음 | 확실 · 기타(연결 표시) | 예 |
|
||||
| QF000003 | 2-1-3 | 우드그래플 소모성 경비(1회 교체당 소요비용) | 끝남 | 4/4 · 2-01_소요재료.md · 2-1-3 | 됨 | 의심 | GF000069 만 소모성경비를 더함 · 같은 우드그래플 품 GF000070 · 314 · 315 는 안 더함 | 의심 · 로직 일부 구현 | |
|
||||
| QF000004 | 2-1-3 | 우드그래플 소모율(Q) 산출 기준 | 끝남 | 17/17 · 2-01_소요재료.md · 2-1-3 | 됨 | 맞음 | GF000069 Q(소모율) 0.02105 | | |
|
||||
| QF000005 | 2-1-4 | 페인트 및 마킹테이프 재료비 | 끝남 | 31/31 · 2-01_소요재료.md · 2-1-4 | 됨 | 의심 | GF000001 · GF000014 에 페인트 줄 없음 · 3-2 로직 없음 | 의심 · 로직 일부 구현 | |
|
||||
| QF000006 | 2-1-4 | 노선·경계 표시 간격 및 재료 소모 기준 | 끝남 | 22/22 · 2-01_소요재료.md · 2-1-4 | 됨 | 의심 | QF000005 와 한 묶음(간격 · 본수 기준) | | |
|
||||
| QF000007 | 2-1-5-1 | 덩굴 약제처리(ha당) | 끝남 | 4/4 · 2-01_소요재료.md · 2-1-5-1 | 됨 | 맞음 | GF000055 농약 10병 · 잡품 10% | | |
|
||||
| QF000008 | 2-1-5-1 | 덩굴 피복도별 농약 소요량 증감률 | 끝남 | 4/4 · 2-01_소요재료.md · 2-1-5-1 | 됨 | 의심 | GF000055 가 두 줄(밀 · 과소)만 씀 — 과밀 +50 · 소 −20 못 씀 | 의심 · 로직 일부 구현 | |
|
||||
| QF000009 | 2-1-5-2 | 소금처리(본당) | 끝남 | 8/8 · 2-01_소요재료.md · 2-1-5-2 | 못 찾음 — 모르는 범위규칙 | 흠(확실) | GF000304 이 못 씀(범위규칙) — 소금 재료도 없음 | 확실 · 기타(표 형식) | |
|
||||
| QF000010 | 2-1-5-2 | 소금처리 잡품율 | 끝남 | 1/1 · 2-01_소요재료.md · 2-1-5-2 | 됨 | 의심 | GF000304 소금 · 잡품 10% 줄 없음 | 의심 · 로직 일부 구현 | |
|
||||
| QF000011 | 2-1-5-3 | 뿌리제거(100본당) | 끝남 | 10/10 · 2-01_소요재료.md · 2-1-5-3 | 됨 | 의심 | GF000305 농약 · 비닐랩 줄 없음 | 의심 · 로직 일부 구현 | |
|
||||
| QF000012 | 2-1-6 | 나무주사(100본당) | 끝남 | 23/23 · 2-01_소요재료.md · 2-1-6 | 됨 | 의심 | GF000317 · 318 재료 줄 없음 · 천공기 휘발유 · 손료 안 씀 | 의심 · 로직 일부 구현 | |
|
||||
| QF000013 | 2-1-6 | 나무주사 ha당 천공수 기준 | 끝남 | 1/1 · 2-01_소요재료.md · 2-1-6 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| QF000014 | 2-1-7 | 소각, 매몰, 훈증, 박피 | 끝남 | 15/15 · 2-01_소요재료.md · 2-1-7 | 됨 | 의심 | GF000078 · 079 · 083 훈증 약제 · 피복제 줄 없음 | 의심 · 로직 일부 구현 | |
|
||||
| QF000015 | 2-1-7 | 훈증 약종별 약제소요량 | 끝남 | 33/33 · 2-01_소요재료.md · 2-1-7 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
| QF000016 | 2-1-8 | 유인 헬기(160ha당) | 끝남 | 9/9 · 2-01_소요재료.md · 2-1-8 | 됨 | 맞음 | GF000081 휘발유 10ℓ · 깃발 8개 · 잡품 95% | | |
|
||||
| QF000017 | 2-1-9 | 드론 무인 헬리콥터(ha당) | 끝남 | 9/9 · 2-01_소요재료.md · 2-1-9 | 됨 | 흠(확실) | GF000320 무인헬기 휘발유 · 잡품 줄 없음 | 확실 · 로직 일부 구현 | |
|
||||
| QF000018 | 2-1-9 | 드론 무인 헬리콥터 방제 가능면적·시간 기준 | 끝남 | 2/2 · 2-01_소요재료.md · 2-1-9 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| QF000019 | 2-1-10 | 드론 무인 멀티콥터(ha당) | 끝남 | 8/8 · 2-01_소요재료.md · 2-1-10 | 됨 | 흠(확실) | GF000320 멀티콥터 휘발유 · 잡품 줄 없음 | 확실 · 로직 일부 구현 | |
|
||||
| QF000020 | 2-1-10 | 드론 무인 멀티콥터 방제 가능면적·시간 기준 | 끝남 | 2/2 · 2-01_소요재료.md · 2-1-10 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| QF000021 | 2-1-11 | 지상 약제살포 | 끝남 | 5/5 · 2-01_소요재료.md · 2-1-11 | 됨 | 흠(확실) | GF000321 경유 · 잡품 줄 없음 | 확실 · 로직 일부 구현 | |
|
||||
| QF000022 | 2-1-12 | 그물망 피복 | 끝남 | 6/6 · 2-01_소요재료.md · 2-1-12 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
| QF000023 | 2-1-13 | 파쇄 소모품 | 끝남 | 7/7 · 2-01_소요재료.md · 2-1-13 | 됨 | 의심 | GF000323 파쇄기날 줄 없음 | 의심 · 로직 일부 구현 | |
|
||||
| QF000024 | 2-1-13 | 파쇄 잡품율 | 끝남 | 1/1 · 2-01_소요재료.md · 2-1-13 | 됨 | 미사용 | 쓰는 로직 없음 | | |
|
||||
|
||||
## 산림 3장 작업장 관리
|
||||
|
||||
| 표 | 절 | 이름 | 표값 | 표값 근거 | 찾기 | 로직 | 로직 근거 | 흠·의심(줄글) | 재확인 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| QF000025 | 3-1 | 경계표시 | 끝남 | 2/2 · 3-01_경계표시.md · 3-1 | 됨 | 맞음 | GF000001 보통인부 0.2인/ha | | |
|
||||
| QF000026 | 3-2 | 작업로 예정선 선정 및 표식 | 끝남 | 2/2 · 3-02_작업로_선정.md · 3-2 | 됨 | 흠(확실) | 3-2 로직 없음(누락) | 확실 · 로직 없음(절 누락) | |
|
||||
| QF000027 | 3-3 | 작업로 설치 | 끝남 | 6/6 · 3-03_작업로_설치.md · 3-3 · 소작업로 · 대작업로 인력구성(병합 칸)을 두 줄에 복사 | 됨 | 맞음 | GF000002 소요 ÷ 벌목부 50% · 보통인부 50% (원문 예시 3.0인씩과 같은 식) | | |
|
||||
| QF000028 | 3-4-2 | 임산물 운반로 및 작업로 신설비 산정(예시) | 끝남 | 13/13 · 3-04_임산물_운반로_신설_및_보수·복구.md · 3-4-2 | 됨 | 의심 | GF000003 Q 3자리(8.946) ↔ 산림 원문 예시 8.95 | 의심 · 계산식 어긋남 | |
|
||||
| QF000029 | 3-4-2 | 임산물 운반로 및 작업로 신설비 횡단경사별 할증률 | 끝남 | 4/4 · 3-04_임산물_운반로_신설_및_보수·복구.md · 3-4-2 | 됨 | 맞음 | GF000004 횡단경사 55 미만 0 · 55~65 미만 10 · 65 이상 20 | | |
|
||||
| QF000030 | 3-4-3 | 임산물 운반로 및 작업로 보수비 - 입지·기상조건별 상 | 끝남 | 6/6 · 3-04_임산물_운반로_신설_및_보수·복구.md · 3-4-3 | 됨 | 맞음 | GF000006 연장 ÷ 기준거리 | | |
|
||||
| QF000031 | 3-4-3 | 임산물 운반로 및 작업로 보수공정표 | 끝남 | 6/6 · 3-04_임산물_운반로_신설_및_보수·복구.md · 3-4-3 | 됨 | 맞음 | GF000005 보통인부 1.5 · 4.0인/100㎡ | | |
|
||||
| QF000032 | 3-5 | 예정지정리 | 끝남 | 36/36 · 3-05_예정지정리.md · 3-5 · 원문 9줄 · 인력구분(병합 칸 「특별인부 50% 보통인부 50%」)을 9줄에 복사 | 됨 | 맞음 | GF000007 소요 × 특별 50% · 보통 50% | | 예 |
|
||||
| QF000033 | 3-5 | 예정지정리 미이용 산림바이오매스 정리량별 할증률 | 끝남 | 4/4 · 3-05_예정지정리.md · 3-5 | 됨 | 맞음 | GF000007 정리량 50 이상 5 · 100 이상 10 | | |
|
||||
| QF000034 | 3-5 | 예정지정리 수확벌채지 임상별 할인·할증률 | 끝남 | 4/4 · 3-05_예정지정리.md · 3-5 | 됨 | 맞음 | GF000007 침엽 +10 · 활엽 −10 | | |
|
||||
| QF000035 | 3-5 | 예정지정리 개벌지 크기별 할증률 | 끝남 | 4/4 · 3-05_예정지정리.md · 3-5 | 됨 | 맞음 | GF000007 개벌지 10 · 5 | | |
|
||||
| QF000036 | 3-5 | 예정지정리 산불피해지 할증률 | 끝남 | 1/1 · 3-05_예정지정리.md · 3-5 | 됨 | 맞음 | GF000007 산불피해 5% × 피해면적 ÷ 조림면적(원문 예시 1.01 과 같음) | | |
|
||||
| QF000037 | 3-6 | 산물 임내정리 | 끝남 | 30/30 · 3-06_산물_임내정리.md · 3-6 | 됨 | 맞음 | GF000008 정리산물 ÷ 품(원문 예시 16 ÷ 4.3) | | |
|
||||
| QF000038 | 3-7 | 재해산물 수집 | 끝남 | 120/120 · 3-07_재해산물_수집.md · 3-7 · 원문 3거리 × 10구간 = 30칸 전부 | 됨 | 맞음 | GF000009 수집량 ÷ 품 × 재선충 상향집재 할증 | | 예 |
|
||||
| QF000039 | 3-8 | 드론 영상 촬영 | 끝남 | 13/13 · 3-08_드론_영상_촬영.md · 3-8 · 원문 5줄 · 빈 칸(-)은 값 없음 | 됨 | 맞음 | GF000010 「계」 줄 0.015 · 0.010 · 0.015 | | |
|
||||
| QF000040 | 3-8 | 드론 영상 촬영 공정별 처리기준 | 끝남 | 12/12 · 3-08_드론_영상_촬영.md · 3-8 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
|
||||
@@ -225,6 +225,7 @@
|
||||
- 이름 조건 = 구분·상세구분·규격이 같아도 품명이 갈리는 자재품목을 가를 때만 둠(예: 파형강관 커플링밴드 vs 접속티 — 둘 다 관경만 규격에 있어 규격 조건으로는 안 갈림). 규격과 같은 방식(낱말이 **모두** 이름에 든 줄) — 대소문자·빈칸은 안 가림.
|
||||
- **키를 못 박음** — 재료 줄은 늘 요소 묶음 + `검색어`(「합판(내수) 12t 서울」) · 줄에 자재품목 키를 바로 적지 않음 · 재료 입력에 키 목록(`고르기`)을 두지 않음.
|
||||
- 찾기 = 검색어 낱말(띄어쓰기로 나눔)이 **모두** 이름 또는 규격에 든 자재품목 줄 · 요소에 `구분` · `상세구분` 이 있으면 그 안에서만 · 단위는 같은 성격까지(아래 환산).
|
||||
- `검색어` 빈 글 = 자재표에 없는 품목(`docs/raw/M01_결정목록` 「자재표에 없는 품목」) — 후보 없음 · 단가 없음 · 까닭만 · 품목을 골라 넣으면 그 값 · 검사는 틀림으로 안 셈.
|
||||
- 자동값 차례 = 값 있는 줄 → 줄 단위와 똑같은 단위 → 검색어 안 지역 낱말(서울 등)과 맞는 줄 → 키 차례 · 그 첫째.
|
||||
- 고른 품목 = 계산 요청에만 실음(`_화면_계약.md` 10장 `고름`) · 로직 파일에 저장하지 않음 · 폐기 품목은 지움.
|
||||
- 단위 환산 = 같은 성격만 — 무게 g · kg · ton(톤 · Ton) / 길이 mm · cm · m · km / 넓이 ㎠ · ㎡ / 부피 ㎤ · ℓ · ㎥ · 단가를 줄 단위로 바꿔 계산.
|
||||
|
||||
@@ -143,7 +143,10 @@ def candidates(
|
||||
"""고르기 조건 안 자재품목 키 — 구분 · 상세구분 · 이름 · 규격 낱말이 모두 든 줄 · 파일 차례대로.
|
||||
`unit` 을 주면 그 단위(호표 줄 단위)와 같은 줄만 — 단위가 다르면 다른 물건.
|
||||
입력 `자재지역` 이 있으면 이름에 그 지역이 든 줄만(그런 줄이 없으면 조건 안 전부).
|
||||
조건에 `검색어` 가 있으면 기본 검색어 찾기(`master_pick`)."""
|
||||
조건에 `검색어` 가 있으면 기본 검색어 찾기(`master_pick`) ·
|
||||
빈 검색어 = 자재표에 없는 품목(후보 없음)."""
|
||||
if mp.empty_term(cond):
|
||||
return []
|
||||
if cond.get("검색어"):
|
||||
return mp.search(master, cond, env, unit)
|
||||
want = [plain_spec(w) for w in str(cond.get("규격") or "").split()]
|
||||
@@ -180,7 +183,7 @@ def pick(master: mf.Master, cond: dict, env: dict, unit: str | None = None) -> s
|
||||
def check_unit(master: mf.Master, where: str, cond: dict, unit: str | None) -> list[str]:
|
||||
"""단위 경고 — 호표 줄·품셈재료 줄 단위와 맞는 후보가 없거나 대표 줄 단위가 다름.
|
||||
틀린 데이터가 아니라 좁혀야 할 조건이라 `check_pick` 과 따로 봄(`check_master 단위`)."""
|
||||
if cond.get("검색어"):
|
||||
if "검색어" in cond: # 빈 검색어(자재표에 없는 품목)도 이 길 — 구분 안 후보로 경고하지 않음
|
||||
return mp.check_warn(master, where, cond, unit)
|
||||
cond = static(cond)
|
||||
rows = master.index.get(MARKET, {})
|
||||
|
||||
@@ -152,6 +152,8 @@ def priced(master, item: dict, ref, env: dict, index: int | None):
|
||||
def _priced(master, item: dict, ref, env: dict, index: int | None):
|
||||
unit = item.get("단위")
|
||||
picked = (CHOSEN.get() or {}).get(str(index)) if index is not None else None
|
||||
if not picked and empty_term(ref): # 자재표에 없는 품목 — 구분 안 첫 품목으로 잇지 않음
|
||||
return None, None, NO_ITEM, None
|
||||
try:
|
||||
if picked or (isinstance(ref, dict) and ref.get("검색어")):
|
||||
ref = picked or pick(master, ref, env, unit)
|
||||
@@ -170,19 +172,29 @@ def _priced(master, item: dict, ref, env: dict, index: int | None):
|
||||
return price, source, why, warn
|
||||
|
||||
|
||||
NO_ITEM = "자재표에 없는 품목 — 검색어 비움(결정 목록) · 품목을 골라 넣음"
|
||||
|
||||
|
||||
def empty_term(ref) -> bool:
|
||||
"""검색어 칸은 있고 비었음 = 자재표에 없는 품목(`M01_결정목록` 「자재표에 없는 품목」) —
|
||||
단가 없음 · 까닭만(고름으로 고를 수 있음) · 검사는 이 줄을 틀림으로 세지 않음."""
|
||||
return isinstance(ref, dict) and "검색어" in ref and not str(ref["검색어"] or "").strip()
|
||||
|
||||
|
||||
# ── 검사 ───────────────────────────────────────────────────────────────
|
||||
def check_zero(master, where: str, cond: dict) -> list[str]:
|
||||
"""검색어 후보 0 — `{입력}` 이 든 검색어는 계산 때 채워져 여기서 못 봄."""
|
||||
"""검색어 후보 0 — `{입력}` 이 든 검색어는 계산 때 채워져 여기서 못 봄 ·
|
||||
빈 검색어는 `empty_term`(틀림 아님)."""
|
||||
term = str(cond.get("검색어") or "")
|
||||
if not term.strip() or "{" in term or any("{" in str(v) for v in cond.values()):
|
||||
return [] if term.strip() else [f"{where} · 검색어 비어 있음"]
|
||||
return []
|
||||
return [] if search(master, cond) else [f"{where} · 검색어 「{term}」 후보 0"]
|
||||
|
||||
|
||||
def check_warn(master, where: str, cond: dict, unit: str | None) -> list[str]:
|
||||
"""단위 경고 줄 — 검색어 후보가 있으나 줄 단위와 같은 성격이 하나도 없음."""
|
||||
term = str(cond.get("검색어") or "")
|
||||
if not unit or "{" in term or "{" in str(cond.get("구분") or ""):
|
||||
if not unit or not term.strip() or "{" in term or "{" in str(cond.get("구분") or ""):
|
||||
return []
|
||||
keys = search(master, cond)
|
||||
rows = master.index.get(mm.MARKET, {})
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": ""},
|
||||
"이름": "동 및 동합금용 용접봉",
|
||||
"규격": "BCUP3",
|
||||
"단위": "㎏",
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": ""},
|
||||
"이름": "잠호용접와이어",
|
||||
"단위": "㎏",
|
||||
"수량": "만약(용접방식 == '자동용접', 환산용접길이 * 찾기(QC001190, 재료='잠호용접와이어').자동용접, 0)",
|
||||
|
||||
@@ -2055,7 +2055,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": "안전발판(PSP대용) 250×3000"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "동바리·비계", "검색어": ""},
|
||||
"이름": "손잡이기둥",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000063, 구분='손잡이기둥').수량 * 찾기(QC000016, 공기=공기, 재료='강관, 비계기본틀, 비계장선틀, 가새').손율 / 100",
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "콘크리트제품", "상세구분": "경계석·블록", "검색어": "주차라인블록 1000X300X150"},
|
||||
"요소": {"구분": "콘크리트제품", "상세구분": "경계석·블록", "검색어": ""},
|
||||
"이름": "치장벽돌",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC001213, 벽두께=벽두께).'벽돌(19×9×5.7㎝)'",
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "방수석고보드 12.5t"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": ""},
|
||||
"이름": "일체식 방수시트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000635, 구분='일체식 방수시트').수량",
|
||||
|
||||
@@ -3303,7 +3303,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": "외송각재 30×30×3600mm 서울"},
|
||||
"요소": {"구분": "목재", "상세구분": "각재·판재", "검색어": ""},
|
||||
"이름": "각재 10㎝×10㎝",
|
||||
"단위": "㎥",
|
||||
"수량": "찾기(QC000245).각재",
|
||||
|
||||
@@ -2394,7 +2394,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프", "검색어": "와이어로프(철심) 9㎜"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "와이어로프·클램프", "검색어": ""},
|
||||
"이름": "P.P로프(9㎜)",
|
||||
"단위": "m",
|
||||
"수량": "찾기(QC000334, 구분='육상부설').'P.P로프(9㎜)' / 100",
|
||||
@@ -5339,7 +5339,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": "직포 옥상방수용직포"},
|
||||
"요소": {"구분": "매트·토목섬유", "상세구분": "부직포·토목섬유", "검색어": ""},
|
||||
"이름": "지오콤포지트",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(재료 == '지오콤포지트', 찾기(QC000399, 구분='지오콤포지트').수량, 0)",
|
||||
|
||||
@@ -957,7 +957,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "처마후레싱 C/S"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": ""},
|
||||
"이름": "테라조",
|
||||
"단위": "m",
|
||||
"수량": "만약(구분 == '석재류', 찾기(QC001266, 구분=구분).테라조, 0)",
|
||||
@@ -966,7 +966,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": "처마후레싱 C/S"},
|
||||
"요소": {"구분": "건축마감", "상세구분": "천장·바닥·마감재", "검색어": ""},
|
||||
"이름": "합성수지 걸레받이",
|
||||
"단위": "m",
|
||||
"수량": "만약(구분 == '합성수지류', 찾기(QC001266, 구분=구분).합성수지, 0)",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": "방수석고보드 12.5t"},
|
||||
"요소": {"구분": "도료·방수·접착", "상세구분": "방수·실링", "검색어": ""},
|
||||
"이름": "벤토나이트방수재",
|
||||
"단위": "㎡",
|
||||
"수량": "만약(종류 == '벤토나이트 시트', 찾기(QC001307, 구분='벤토나이트방수재', 종류='시트', 부위=부위).수량, 0)",
|
||||
@@ -1021,7 +1021,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "PVC코팅철선 #8"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": ""},
|
||||
"이름": "조인트테이프",
|
||||
"단위": "m",
|
||||
"수량": "만약(종류 == '벤토나이트 시트', 찾기(QC001307, 구분='조인트테이프', 종류='시트', 부위=부위).수량, 0)",
|
||||
|
||||
@@ -1272,7 +1272,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "패널(K-SPAN PANEL) 0.6×305mm"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""},
|
||||
"이름": "강재거푸집 손료",
|
||||
"단위": "㎡",
|
||||
"수량": "(1 - 10 / 100) / 사용횟수",
|
||||
@@ -1583,7 +1583,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""},
|
||||
"이름": "원형맨홀거푸집 ø740 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø740', 1 / 10, 0)",
|
||||
@@ -1592,7 +1592,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""},
|
||||
"이름": "원형맨홀거푸집 ø900 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø900', 1 / 10, 0)",
|
||||
@@ -1601,7 +1601,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""},
|
||||
"이름": "원형맨홀거푸집 ø1200 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø1200', 1 / 10, 0)",
|
||||
@@ -1610,7 +1610,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""},
|
||||
"이름": "원형맨홀거푸집 ø1500 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø1500', 1 / 10, 0)",
|
||||
@@ -1619,7 +1619,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""},
|
||||
"이름": "원형맨홀거푸집 ø1800 손료",
|
||||
"단위": "조",
|
||||
"수량": "만약(규격 == 'ø1800', 1 / 10, 0)",
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트", "검색어": "작은나사(스크류) ø 4.2㎜"},
|
||||
"요소": {"구분": "볼트·앵커·철물", "상세구분": "볼트·너트", "검색어": ""},
|
||||
"이름": "AL리벳",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC001339, 구분='AL리벳', 규격='ø 4.2㎜').수량",
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": ""},
|
||||
"이름": "샘플러",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000779, 종별='샘플러').수량",
|
||||
@@ -522,7 +522,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": ""},
|
||||
"이름": "베인 블레이드",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000780, 종별='재료비', 세목='vane blade(대형)').수량",
|
||||
@@ -1545,7 +1545,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": ""},
|
||||
"이름": "에어해머",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000794, 종별='에어해머').수량",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": ""},
|
||||
"이름": "에어해머",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000795, 종별='에어해머').수량",
|
||||
@@ -1789,7 +1789,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": ""},
|
||||
"이름": "에어해머",
|
||||
"단위": "개",
|
||||
"수량": "찾기(QC000796, 종별='에어해머').수량",
|
||||
|
||||
@@ -6918,7 +6918,7 @@
|
||||
{ "종류": "재료", "요소": { "구분": "잡재료", "상세구분": "소모품", "검색어": "여과기 14\"×14〃" }, "이름": "여과기", "단위": "Sh", "수량": "3", "비목": "재료비", "출처": "건설품셈 기계설비 13-6-1" },
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": { "구분": "잡재료", "상세구분": "소모품", "검색어": "그리스(일반범용) CROWN" },
|
||||
"요소": { "구분": "잡재료", "상세구분": "소모품", "검색어": "" },
|
||||
"이름": "걸레",
|
||||
"단위": "㎏",
|
||||
"수량": "2.5",
|
||||
@@ -7200,7 +7200,7 @@
|
||||
{ "종류": "재료", "요소": { "구분": "잡재료", "상세구분": "용접·가스", "검색어": "용접봉 산소" }, "이름": "가스용접봉", "단위": "㎏", "수량": "0.146", "비목": "재료비", "출처": "건설품셈 기계설비 13-6-2" },
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": { "구분": "잡재료", "상세구분": "용접·가스", "검색어": "아세틸렌 98%" },
|
||||
"요소": { "구분": "잡재료", "상세구분": "용접·가스", "검색어": "" },
|
||||
"이름": "신주용접봉",
|
||||
"단위": "㎏",
|
||||
"수량": "0.073",
|
||||
@@ -7220,7 +7220,7 @@
|
||||
{ "종류": "재료", "요소": { "구분": "잡재료", "상세구분": "소모품", "검색어": "소창직" }, "이름": "소창직", "단위": "m", "수량": "0.134", "비목": "재료비", "출처": "건설품셈 기계설비 13-6-2" },
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": { "구분": "잡재료", "상세구분": "소모품", "검색어": "그리스(일반범용) CROWN" },
|
||||
"요소": { "구분": "잡재료", "상세구분": "소모품", "검색어": "" },
|
||||
"이름": "걸레",
|
||||
"단위": "㎏",
|
||||
"수량": "0.73",
|
||||
@@ -10333,7 +10333,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": ""},
|
||||
"이름": "Bite",
|
||||
"단위": "개",
|
||||
"수량": "2.5",
|
||||
@@ -10342,7 +10342,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "빗자루 도로청소용"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": ""},
|
||||
"이름": "톱날",
|
||||
"단위": "개",
|
||||
"수량": "2.5",
|
||||
@@ -10351,7 +10351,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "치즐(0.2㎥)"},
|
||||
"요소": {"구분": "기계·설비", "상세구분": "기계류", "검색어": ""},
|
||||
"이름": "Bite",
|
||||
"단위": "개",
|
||||
"수량": "3.2",
|
||||
@@ -13159,7 +13159,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "그리스(일반범용) CROWN"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": ""},
|
||||
"이름": "가루비누",
|
||||
"단위": "㎏",
|
||||
"수량": "0.04",
|
||||
@@ -15685,7 +15685,7 @@
|
||||
"출처": "건설품셈 기계설비 13-10-2",
|
||||
"비고": "원문 규격 ø4㎜×ℓ350"
|
||||
},
|
||||
{ "종류": "재료", "요소": { "구분": "잡재료", "상세구분": "소모품", "검색어": "그리스(일반범용) CROWN" }, "이름": "걸레", "단위": "㎏", "수량": "2", "비목": "재료비", "출처": "건설품셈 기계설비 13-10-2" },
|
||||
{ "종류": "재료", "요소": { "구분": "잡재료", "상세구분": "소모품", "검색어": "" }, "이름": "걸레", "단위": "㎏", "수량": "2", "비목": "재료비", "출처": "건설품셈 기계설비 13-10-2" },
|
||||
{ "종류": "재료", "요소": { "구분": "잡재료", "상세구분": "소모품", "대표": "MT030665" }, "이름": "세유", "단위": "ℓ", "수량": "2", "비목": "재료비", "출처": "건설품셈 기계설비 13-10-2" },
|
||||
{ "종류": "재료", "요소": { "구분": "잡재료", "상세구분": "소모품", "대표": "MT030646" }, "이름": "Grease", "단위": "㎏", "수량": "0.2", "비목": "재료비", "출처": "건설품셈 기계설비 13-10-2" },
|
||||
{
|
||||
@@ -16505,7 +16505,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": "그리스(일반범용) CROWN"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "소모품", "검색어": ""},
|
||||
"이름": "걸레",
|
||||
"단위": "㎏",
|
||||
"수량": "(만약(공사 == '기기', 0.73, 만약(공사 == '철골', 0.1, 만약(공사 == '배관', 0.2, 만약(공사 == 'Belt & Conveyor', 0.3, 만약(공사 == 'Heater & Tank', 0.1, 만약(공사 == 'Pump & Fan', 0.73, 만약(공사 == 'Crane 류', 0.73, 0))))))))",
|
||||
@@ -16649,7 +16649,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "팁(날) 노면파쇄기용"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": ""},
|
||||
"이름": "용접면",
|
||||
"단위": "개",
|
||||
"수량": "(만약(공사 == '기기', 0.01, 만약(공사 == '철골', 0.02, 만약(공사 == '배관', 0.02, 만약(공사 == 'Belt & Conveyor', 0.02, 만약(공사 == 'Heater & Tank', 0.02, 만약(공사 == 'Pump & Fan', 0.01, 만약(공사 == 'Crane 류', 0.02, 0))))))))",
|
||||
@@ -16658,7 +16658,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "팁(날) 노면파쇄기용"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": ""},
|
||||
"이름": "용접홀다",
|
||||
"단위": "개",
|
||||
"수량": "(만약(공사 == '기기', 0.01, 만약(공사 == '철골', 0.02, 만약(공사 == '배관', 0.02, 만약(공사 == 'Belt & Conveyor', 0.02, 만약(공사 == 'Heater & Tank', 0.02, 만약(공사 == 'Pump & Fan', 0.01, 만약(공사 == 'Crane 류', 0.02, 0))))))))",
|
||||
@@ -16667,7 +16667,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": "팁(날) 노면파쇄기용"},
|
||||
"요소": {"구분": "잡재료", "상세구분": "용접·가스", "검색어": ""},
|
||||
"이름": "용접앞치마",
|
||||
"단위": "개",
|
||||
"수량": "(만약(공사 == '기기', 0.01, 만약(공사 == '철골', 0.05, 만약(공사 == '배관', 0.02, 만약(공사 == 'Belt & Conveyor', 0.05, 만약(공사 == 'Heater & Tank', 0.05, 만약(공사 == 'Pump & Fan', 0.01, 만약(공사 == 'Crane 류', 0.05, 0))))))))",
|
||||
|
||||
@@ -944,7 +944,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "비탈형영구거푸집(KC-FORM) 450용"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""},
|
||||
"이름": "핀폴 손료",
|
||||
"단위": "개",
|
||||
"수량": "1 * (1 - 10 / 100) / 20",
|
||||
@@ -3198,7 +3198,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""},
|
||||
"이름": "원형거푸집(PE 10회) 스라브 및 기초",
|
||||
"단위": "조",
|
||||
"수량": "1 / 10",
|
||||
@@ -3208,7 +3208,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": "폼타이 D형"},
|
||||
"요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""},
|
||||
"이름": "원형거푸집(PE 10회) 벽체",
|
||||
"단위": "조",
|
||||
"수량": "1 / 10",
|
||||
@@ -4656,7 +4656,7 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "교량·구조부속", "상세구분": "신축이음·받침", "검색어": "익스팬션조인트휠러 t=10mm"},
|
||||
"요소": {"구분": "교량·구조부속", "상세구분": "신축이음·받침", "검색어": ""},
|
||||
"이름": "탄성고무받침(150×150×15)",
|
||||
"단위": "㎡",
|
||||
"수량": "고무받침면적",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""B01 대시보드 「시스템 설정」 컨테이너 시험(27-3) — 화면 조작은 ORCA.
|
||||
|
||||
· 역할별 카드(제목 · 설명 · 이동 해시)를 Node 로 바로 돌림 — 가짜 DOM
|
||||
· 모든 역할 맨 위 한 줄 · 통째로 접힘 · 「구조물 관리」 컨테이너 없음
|
||||
· 모든 역할 한 줄 · 통째로 접힘 · 「구조물 관리」 컨테이너 없음
|
||||
· 회사 고르기(29-1) — 시스템 관리자만 · 리소스 현황 아래 · 해시 company · 서버 인자 한 곳
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -28,16 +29,18 @@ def _fn(src: str, name: str) -> str:
|
||||
return found.group(0)
|
||||
|
||||
|
||||
def _cards(user: dict) -> list:
|
||||
def _cards(user: dict, company_id: int | None = None) -> list:
|
||||
src = _text(PAGE)
|
||||
script = (
|
||||
"interface DashboardUser { role: string; is_master: boolean }\n"
|
||||
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data', M02_MASTER_TEMPLATE: 'm02-master-template' };\n"
|
||||
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data',"
|
||||
" M02_MASTER_TEMPLATE: 'm02-master-template' };\n"
|
||||
"const L = (k: string) => k;\n"
|
||||
"let went = '';\n"
|
||||
"const navigateTo = (r: string, q?: Record<string, string>) =>"
|
||||
" { went = r + (q ? '?' + new URLSearchParams(q) : ''); };\n"
|
||||
"const mk = (): any => ({ children: [] as any[], textContent: '', className: '', on: null as any,"
|
||||
"const mk = (): any => ({ children: [] as any[], textContent: '', className: '',"
|
||||
" on: null as any,"
|
||||
" append(...c: any[]) { this.children.push(...c); },"
|
||||
" addEventListener(_: string, f: any) { this.on = f; } });\n"
|
||||
"const document = { createElement: () => mk() };\n"
|
||||
@@ -45,13 +48,14 @@ def _cards(user: dict) -> list:
|
||||
+ "\n"
|
||||
+ _fn(src, "buildSystemSettingsPanel")
|
||||
+ "\nconst user = JSON.parse(process.argv[1]);"
|
||||
+ "\nconsole.log(JSON.stringify(buildSystemSettingsPanel(user).children.map((c: any) => {"
|
||||
+ "\nconst panel = buildSystemSettingsPanel(user, JSON.parse(process.argv[2]));"
|
||||
+ "\nconsole.log(JSON.stringify(panel.children.map((c: any) => {"
|
||||
+ " c.on(); return [c.children[0].textContent, c.children[1].textContent,"
|
||||
+ " decodeURIComponent(went)]; })));"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", script, json.dumps(user)],
|
||||
"--input-type=module-typescript", "-e", script, json.dumps(user), json.dumps(company_id)],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
@@ -66,15 +70,19 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite")
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_시스템_관리자_카드_다섯():
|
||||
def test_시스템_관리자_카드_다섯_고른_회사_해시():
|
||||
m = "B01_Dashboard_PermMasterWrite"
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}) == [
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [
|
||||
["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"],
|
||||
["B01_Dashboard_CardUnitCombo", m, f"{M02}?open=일위대가&root=master"],
|
||||
["B01_Dashboard_CardStructCombo", m, f"{M02}?open=구조물조합&root=master"],
|
||||
["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master"],
|
||||
["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master"],
|
||||
["B01_Dashboard_CardUnitCombo", m, f"{M02}?open=일위대가&root=master&company=3"],
|
||||
["B01_Dashboard_CardStructCombo", m, f"{M02}?open=구조물조합&root=master&company=3"],
|
||||
["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"],
|
||||
["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"],
|
||||
]
|
||||
# 회사가 없으면 해시에 안 실음
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[1][2] == (
|
||||
f"{M02}?open=일위대가&root=master"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
@@ -83,7 +91,7 @@ def test_회사_관리자_개인_카드_둘():
|
||||
["B01_Dashboard_CardUnitCombo", WRITE_CO, f"{M02}?open=일위대가&root=company"],
|
||||
["B01_Dashboard_CardStructCombo", WRITE_CO, f"{M02}?open=구조물조합&root=company"],
|
||||
]
|
||||
assert _cards({"role": "ADMIN", "is_master": False}) == company
|
||||
assert _cards({"role": "ADMIN", "is_master": False}, 3) == company # 회사 인자 안 실음
|
||||
assert _cards({"role": "USER", "is_master": True}) == company # 회사 마스터 = 회사 수정
|
||||
assert _cards({"role": "USER", "is_master": False}) == [
|
||||
["B01_Dashboard_CardUnitCombo", READ_CO, f"{M02}?open=일위대가&root=personal"],
|
||||
@@ -91,18 +99,91 @@ def test_회사_관리자_개인_카드_둘():
|
||||
]
|
||||
|
||||
|
||||
def test_모든_역할_맨_위_통째로_접힘():
|
||||
def test_모든_역할_한_줄_통째로_접힘():
|
||||
src = _text(PAGE)
|
||||
build = _fn(src, "buildPage")
|
||||
# 역할 갈래 앞에서 한 번 — 모든 역할 · 프로젝트 위
|
||||
assert build.index("grid.append(buildSystemSettingsSection(state.user))") < build.index(
|
||||
'if (state.user.role === "SYSTEM_ADMIN")'
|
||||
# 세 갈래 모두 한 번씩 · 시스템 관리자는 리소스 현황 아래(29-1) · 프로젝트 위
|
||||
assert build.count("buildSystemSettingsSection(state)") == 3
|
||||
sys_part = build.split('} else if (state.user.role === "ADMIN")')[0]
|
||||
assert (
|
||||
sys_part.index('L("B01_Dashboard_Resources")')
|
||||
< sys_part.index("buildSystemSettingsSection(state)")
|
||||
< sys_part.index('L("B01_Dashboard_Projects")')
|
||||
)
|
||||
assert "StructureManage" not in src and "buildStructurePanel" not in src
|
||||
assert "top-row" not in src
|
||||
sect = _fn(src, "buildSystemSettingsSection")
|
||||
assert "buildSystemSettingsPanel(user), true)" in sect # 한 줄 통째
|
||||
assert 'L("B01_Dashboard_SystemSettings"), panel, true, pill)' in sect # 한 줄 통째
|
||||
assert '"ui-collapsible"' in sect and '"ui-collapsible__title"' in sect
|
||||
assert "attachCollapsible(card)" in sect
|
||||
config = _text(ROOT / "config" / "config_frontend.ts")
|
||||
assert re.search(r"PROTECTED_ROUTES[\s\S]*?ROUTES\.M02_MASTER_TEMPLATE,[\s\S]*?\];", config)
|
||||
|
||||
|
||||
def test_회사_알약_시스템_관리자만():
|
||||
src = _text(PAGE)
|
||||
sect = _fn(src, "buildSystemSettingsSection")
|
||||
assert 'user.role === "SYSTEM_ADMIN" ? [companyPill(state)] : []' in sect
|
||||
pill = _fn(src, "companyPill")
|
||||
assert "sessionStorage.setItem(ADMIN_COMPANY_KEY, String(c.id))" in pill
|
||||
assert "event.stopPropagation()" in pill # 알약 누름이 접힘으로 새지 않음
|
||||
assert 'createMapContextMenu("b01-dashboard")' in pill # 공용 작은 메뉴
|
||||
assert '"ui-tag ui-tag--accent b01-dashboard__company-pill"' in pill # 공용 알약 모양
|
||||
assert 'const ADMIN_COMPANY_KEY = "aislo.adminCompany";' in src # M02 와 같은 키
|
||||
load = src.split("async function loadRoleData")[1].split("\n}\n")[0]
|
||||
for call in ("fetchAllUsers(companyId)", "fetchCompanyJoinRequests(companyId)",
|
||||
"fetchCompanyProjects(companyId)"): # fmt: skip
|
||||
assert call in load, call
|
||||
build = _fn(src, "buildPage")
|
||||
# 회사 따라 바뀌는 컨테이너 넷(프로젝트 · 사용자 관리 · 가입 요청 · 회사 관리)마다 알약
|
||||
sys_part = build.split('} else if (state.user.role === "ADMIN")')[0]
|
||||
assert sys_part.count("companyPill(state)") == 4
|
||||
assert "companyPill" not in build.split('} else if (state.user.role === "ADMIN")')[1]
|
||||
assert "companyTable(picked, state.user)" in build
|
||||
assert "openAddMemberModal(undefined, state.adminCompanyId)" in build
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_고른_회사_저장값_검사():
|
||||
src = _text(PAGE)
|
||||
script = (
|
||||
"interface DashboardUser { company_id?: number | null }\n"
|
||||
"interface CompanyInfo { id: number }\n"
|
||||
"const ADMIN_COMPANY_KEY = 'aislo.adminCompany';\n"
|
||||
"const store: Record<string, string> = {};\n"
|
||||
"const sessionStorage = { getItem: (k: string) => store[k] ?? null };\n"
|
||||
+ _fn(src, "pickAdminCompany")
|
||||
+ "\nconst cs = [{ id: 1 }, { id: 3 }];"
|
||||
+ "\nconst out = [pickAdminCompany({ company_id: 1 }, cs)];"
|
||||
+ "\nstore[ADMIN_COMPANY_KEY] = '3'; out.push(pickAdminCompany({ company_id: 1 }, cs));"
|
||||
+ "\nstore[ADMIN_COMPANY_KEY] = '9'; out.push(pickAdminCompany({ company_id: 1 }, cs));"
|
||||
+ "\nout.push(pickAdminCompany({ company_id: null }, cs));"
|
||||
+ "\nconsole.log(JSON.stringify(out));"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", script],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
# 기본 자기 회사 · 고른 회사 · 없는 회사면 자기 회사 · 자기 회사 없으면 첫 회사
|
||||
assert json.loads(result.stdout) == [1, 3, 1, 1]
|
||||
|
||||
|
||||
def test_서버_회사_인자_한_곳():
|
||||
scope = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_Scope.py")
|
||||
assert 'if session["role"] == "SYSTEM_ADMIN" and company_id is not None:' in scope
|
||||
router = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_Router.py")
|
||||
for name in (
|
||||
"admin_add_member",
|
||||
"admin_invite_member",
|
||||
"admin_join_requests",
|
||||
"admin_projects",
|
||||
):
|
||||
body = router.split(f"async def {name}(")[1].split("@router.")[0]
|
||||
assert "company_id: int | None = Query(None, gt=0)" in body, name
|
||||
assert "_scope_company(session, company_id)" in body, name
|
||||
users = router.split("async def system_users(")[1].split("@router.")[0]
|
||||
assert "require_system_admin" in users and "list_all_users(company_id)" in users
|
||||
assert "router.include_router(assets_router)" in router
|
||||
assert len(router.splitlines()) <= 700
|
||||
|
||||
@@ -770,8 +770,18 @@ def test_품셈재료_자재지역_후보(whole):
|
||||
"그룹": "재료",
|
||||
"원문": "품셈재료",
|
||||
"줄": [
|
||||
{"키": "MP900003", "이름": "모래", "단위": "㎥", "연결": {"이름": "모래", "규격": "", "지역": "입력"}},
|
||||
{"키": "MP900004", "이름": "레미콘", "단위": "㎥", "연결": {"이름": "레미콘", "규격": "", "지역": "입력"}},
|
||||
{
|
||||
"키": "MP900003",
|
||||
"이름": "모래",
|
||||
"단위": "㎥",
|
||||
"연결": {"이름": "모래", "규격": "", "지역": "입력"},
|
||||
},
|
||||
{
|
||||
"키": "MP900004",
|
||||
"이름": "레미콘",
|
||||
"단위": "㎥",
|
||||
"연결": {"이름": "레미콘", "규격": "", "지역": "입력"},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1168,3 +1178,15 @@ def test_변수_참조_줄은_고르기_조건으로_바뀜(whole):
|
||||
and "{" in item["요소"]
|
||||
]
|
||||
assert sorted(남음) == []
|
||||
|
||||
|
||||
def test_빈_검색어는_자재표에_없는_품목_단가_없음_검사_통과():
|
||||
"""PLAN 29-3 — 빈 검색어 = 자재표에 없는 품목: 구분 안 첫 품목으로 안 이음 ·
|
||||
계산은 까닭만 · 틀림 아님."""
|
||||
whole = cm.master()
|
||||
cond = {"구분": "거푸집·가설재", "상세구분": "거푸집", "검색어": ""}
|
||||
assert mm.candidates(whole, cond) == []
|
||||
assert mm.check_pick(whole, "시험", cond) == []
|
||||
got = mf.run(whole, "GF000165", cm.시험입력(whole.logic("GF000165")))
|
||||
line = next(x for x in got["줄"] if x["이름"] == "핀폴 손료")
|
||||
assert line["단가"] is None and "자재표에 없는 품목" in line["까닭"]
|
||||
|
||||
@@ -145,6 +145,7 @@ export const ui_locales_b3 = {
|
||||
B01_Dashboard_PermCompanyRead: ["회사 읽기", "Company read"],
|
||||
B01_Dashboard_PermCompanyWrite: ["회사 수정", "Company edit"],
|
||||
B01_Dashboard_PermPersonalWrite: ["개인 수정", "Personal edit"],
|
||||
B01_Dashboard_PickCompany: ["회사 고르기", "Pick company"],
|
||||
|
||||
/* --- B08·B09 빈 화면 (PLAN 7-3) --- */
|
||||
B08_Quantity_Rebuilding: [
|
||||
|
||||
Reference in New Issue
Block a user