Merge remote-tracking branch 'origin/dev' into sub_laptop_7

This commit is contained in:
2026-09-30 17:43:31 +09:00
32 changed files with 847 additions and 268 deletions
+24 -10
View File
@@ -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;
}
+5 -3
View File
@@ -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:
+30 -166
View File
@@ -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)
+36
View File
@@ -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
+6 -3
View File
@@ -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);
});
+92 -21
View File
@@ -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) }),
]),
+53
View File
@@ -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;
+41
View File
@@ -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 | 검색어 비움 |
+93
View File
@@ -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 는 안 더함 | 의심
+126
View File
@@ -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 | 됨 | 해당 없음 | 참고 · 정의 · 표시 규칙 표 — 계산식 재료 아님 | | |
+1
View File
@@ -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, {})
+15 -3
View File
@@ -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)",
"단위": "㎡",
"수량": "고무받침면적",
+99 -18
View File
@@ -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
+24 -2
View File
@@ -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["까닭"]
+1
View File
@@ -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: [