feat(B01,B07): 표지 연도·기번·사업량을 사람이 넣는 자리 신설
표제란에 남아 있던 빈칸 중 연도·기번과 사업량은 프로그램이 지어낼 수 없는 값이라
입력 자리를 만든다. 값의 출처를 정하지 않고 받을 칸만 세운다.
DB (`012_title_block_inputs.sql`, ADD COLUMN·NULL 허용)
- `projects.project_number` — 표지 연도·기번(실무문서 폴더명 관행:
`2024년 간선임도(기번3-울진.대흥)`)
- `projects.work_amount` — 표지 사업량. 단위·표기가 사업 종류마다 달라 자유 문자열.
B01 프로젝트 수정
- 수정 모달에 시행청·연도기번·사업량 3칸 추가. 011 로 만든 시행청도 여기서 처음 입력
가능해짐(그전에는 스크립트로만 넣었음).
- 목록·단건 조회 4곳과 UPDATE 에 세 칸을 함께 실음. USER 권한은 다른 칸과 같이 잠금.
B07
- `_title_block_fields` 가 연도기번·사업량도 실어 보냄. 비어 있으면 종전대로 빈칸.
검증: `pytest tmp/tests/ -q` 139 passed / 0 failed(표지 값 1건 신규), 루트 `tsc --noEmit`
통과. 실사용자 경로 실측(5174) — 대시보드 wdw 행 [수정] 클릭 → 모달 라벨 9개에 새 3칸
확인 → 연도기번·사업량 입력 → [확인] 저장 → 표지 도면 Text 7개 **빈칸 0 · `{{` 잔존 0**,
화면에도 연도기번·`wdw 설계도`·위치·사업량·시행청이 모두 그려짐.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,10 @@ export interface ProjectItem {
|
||||
estimated_length_m?: number | null;
|
||||
memo?: string | null;
|
||||
status?: string | null;
|
||||
/** 도면 표제란·표지에 실리는 값 — 프로그램이 지어낼 수 없어 사람이 넣는다 */
|
||||
client_org?: string | null;
|
||||
project_number?: string | null;
|
||||
work_amount?: string | null;
|
||||
owner_name?: string | null;
|
||||
workflow_stage: number;
|
||||
progress_percent: number;
|
||||
@@ -132,6 +136,9 @@ export interface UpdateProjectRequest {
|
||||
estimated_length_m?: number | null;
|
||||
memo?: string | null;
|
||||
status?: string | null;
|
||||
client_org?: string | null;
|
||||
project_number?: string | null;
|
||||
work_amount?: string | null;
|
||||
}
|
||||
|
||||
export interface AdminUpdateUserRequest extends UpdateUserRequest {
|
||||
|
||||
@@ -148,7 +148,8 @@ async def list_user_projects(user_id: int) -> list[dict[str, Any]]:
|
||||
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"""SELECT id, company_id, name, region, road_type, project_year,
|
||||
estimated_length_m, memo, status, updated_at, created_at
|
||||
estimated_length_m, memo, status, updated_at, created_at,
|
||||
client_org, project_number, work_amount
|
||||
FROM projects WHERE user_id = %s AND deleted_at IS NULL
|
||||
ORDER BY updated_at DESC, created_at DESC""",
|
||||
(user_id,),
|
||||
@@ -170,6 +171,7 @@ async def list_company_projects(company_id: int) -> list[dict[str, Any]]:
|
||||
await cursor.execute(
|
||||
"""SELECT p.id, p.company_id, p.name, p.region, p.road_type, p.project_year,
|
||||
p.estimated_length_m, p.memo, p.status, p.updated_at, p.created_at,
|
||||
p.client_org, p.project_number, p.work_amount,
|
||||
u.name AS owner_name, u.email AS owner_email
|
||||
FROM projects p LEFT JOIN users u ON u.id = p.user_id
|
||||
WHERE p.company_id = %s AND p.deleted_at IS NULL
|
||||
@@ -193,6 +195,7 @@ async def list_all_projects() -> list[dict[str, Any]]:
|
||||
await cursor.execute(
|
||||
"""SELECT p.id, p.company_id, p.name, p.region, p.road_type, p.project_year,
|
||||
p.estimated_length_m, p.memo, p.status, p.updated_at, p.created_at,
|
||||
p.client_org, p.project_number, p.work_amount,
|
||||
u.name AS owner_name, u.email AS owner_email
|
||||
FROM projects p LEFT JOIN users u ON u.id = p.user_id
|
||||
WHERE p.deleted_at IS NULL
|
||||
@@ -214,7 +217,8 @@ async def get_project(project_id: str) -> dict[str, Any] | None:
|
||||
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"""SELECT id, user_id, company_id, name, region, road_type, project_year,
|
||||
estimated_length_m, memo, status
|
||||
estimated_length_m, memo, status,
|
||||
client_org, project_number, work_amount
|
||||
FROM projects WHERE id = %s AND deleted_at IS NULL""",
|
||||
(project_id,),
|
||||
)
|
||||
@@ -228,7 +232,8 @@ async def update_project(project_id: str, data: dict[str, Any], actor_id: int) -
|
||||
await cursor.execute(
|
||||
"""UPDATE projects
|
||||
SET name = %s, region = %s, road_type = %s, project_year = %s,
|
||||
estimated_length_m = %s, memo = %s, status = COALESCE(%s, status)
|
||||
estimated_length_m = %s, memo = %s, status = COALESCE(%s, status),
|
||||
client_org = %s, project_number = %s, work_amount = %s
|
||||
WHERE id = %s AND deleted_at IS NULL""",
|
||||
(
|
||||
data["name"],
|
||||
@@ -238,6 +243,9 @@ async def update_project(project_id: str, data: dict[str, Any], actor_id: int) -
|
||||
data.get("estimated_length_m"),
|
||||
data.get("memo"),
|
||||
data.get("status"),
|
||||
data.get("client_org"),
|
||||
data.get("project_number"),
|
||||
data.get("work_amount"),
|
||||
project_id,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -46,6 +46,10 @@ class UpdateProjectRequest(BaseModel):
|
||||
estimated_length_m: float | None = Field(default=None, ge=0)
|
||||
memo: str | None = Field(default=None, max_length=5000)
|
||||
status: str | None = Field(default=None, max_length=50)
|
||||
# 도면 표제란·표지에 실리는 값 (2026-09-02). 프로그램이 지어낼 수 없어 사람이 넣는다.
|
||||
client_org: str | None = Field(default=None, max_length=255)
|
||||
project_number: str | None = Field(default=None, max_length=100)
|
||||
work_amount: str | None = Field(default=None, max_length=100)
|
||||
|
||||
|
||||
class AdminUpdateUserRequest(UpdateUserRequest):
|
||||
|
||||
@@ -90,6 +90,21 @@ export function openEditProjectModal(user: DashboardUser, project: ProjectItem):
|
||||
value: String(project.estimated_length_m ?? ""),
|
||||
});
|
||||
const memo = createInputField({ label: "비고", value: project.memo ?? "" });
|
||||
// 도면 표제란·표지에 그대로 실리는 값 — 프로그램이 지어낼 수 없어 여기서 받는다.
|
||||
const clientOrg = createInputField({
|
||||
label: "시행청 (도면 표제란)",
|
||||
value: project.client_org ?? "",
|
||||
});
|
||||
const projectNumber = createInputField({
|
||||
label: "연도·기번 (표지)",
|
||||
value: project.project_number ?? "",
|
||||
placeholder: "예: 2026년 간선임도(기번3-울진.대흥)",
|
||||
});
|
||||
const workAmount = createInputField({
|
||||
label: "사업량 (표지)",
|
||||
value: project.work_amount ?? "",
|
||||
placeholder: "예: L=2.14km",
|
||||
});
|
||||
|
||||
if (isUserOnly) {
|
||||
name.input.disabled = true;
|
||||
@@ -98,11 +113,24 @@ export function openEditProjectModal(user: DashboardUser, project: ProjectItem):
|
||||
year.input.disabled = true;
|
||||
length.input.disabled = true;
|
||||
memo.input.disabled = true;
|
||||
clientOrg.input.disabled = true;
|
||||
projectNumber.input.disabled = true;
|
||||
workAmount.input.disabled = true;
|
||||
}
|
||||
|
||||
openModal(
|
||||
L("B01_Dashboard_EditProject"),
|
||||
[name.root, region.root, roadType.root, year.root, length.root, memo.root],
|
||||
[
|
||||
name.root,
|
||||
region.root,
|
||||
roadType.root,
|
||||
year.root,
|
||||
length.root,
|
||||
memo.root,
|
||||
clientOrg.root,
|
||||
projectNumber.root,
|
||||
workAmount.root,
|
||||
],
|
||||
async () => {
|
||||
await updateProject(project.id, {
|
||||
name: name.input.value.trim(),
|
||||
@@ -112,6 +140,9 @@ export function openEditProjectModal(user: DashboardUser, project: ProjectItem):
|
||||
estimated_length_m: length.input.value ? Number(length.input.value) : null,
|
||||
memo: memo.input.value.trim() || null,
|
||||
status: project.status,
|
||||
client_org: clientOrg.input.value.trim() || null,
|
||||
project_number: projectNumber.input.value.trim() || null,
|
||||
work_amount: workAmount.input.value.trim() || null,
|
||||
});
|
||||
showToast(L("B01_Dashboard_Saved"), "success");
|
||||
},
|
||||
|
||||
@@ -122,7 +122,7 @@ async def _title_block_fields(project_id: UUID) -> dict[str, str]:
|
||||
배정이 없으면 프로젝트 소유자로 떨어진다 — 혼자 쓰는 계정에서도 칸이 차게.
|
||||
|
||||
아직 못 채우는 자리와 이유:
|
||||
- 축척(A1/A3)·사업량·연도기번 — 값을 지어내지 않는다(임의 수치 금지).
|
||||
- 사업량·연도기번은 사람이 넣는 값이다(B01 프로젝트 수정 화면). 비어 있으면 빈칸.
|
||||
- 설계일자 — "확정일"인데 도각은 **확정 전**에 그려져 저장본에 굳는다.
|
||||
채울 시점 정의가 미결이라 비워 둔다.
|
||||
- 도면번호 — 단건 조회가 목록 순서를 모른다(목록을 다시 만들면 도면을 열 때마다
|
||||
@@ -132,7 +132,8 @@ async def _title_block_fields(project_id: UUID) -> dict[str, str]:
|
||||
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT p.name, p.region, p.client_org, c.name, c.logo_path,
|
||||
SELECT p.name, p.region, p.client_org, p.project_number, p.work_amount,
|
||||
c.name, c.logo_path,
|
||||
COALESCE(designer.name, owner.name),
|
||||
COALESCE(designer.signature_path, owner.signature_path),
|
||||
pm.name, lead.name
|
||||
@@ -149,11 +150,25 @@ async def _title_block_fields(project_id: UUID) -> dict[str, str]:
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
return {}
|
||||
name, region, client_org, company, logo_path, designer, signature_path, pm, lead = row
|
||||
(
|
||||
name,
|
||||
region,
|
||||
client_org,
|
||||
project_number,
|
||||
work_amount,
|
||||
company,
|
||||
logo_path,
|
||||
designer,
|
||||
signature_path,
|
||||
pm,
|
||||
lead,
|
||||
) = row
|
||||
fields = {
|
||||
"공사명": name,
|
||||
"위치": region,
|
||||
"시행청": client_org,
|
||||
"연도기번": project_number,
|
||||
"사업량": work_amount,
|
||||
"용역회사": company,
|
||||
"설계자": designer,
|
||||
"과업책임자": pm,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 012_title_block_inputs.sql
|
||||
-- 표지에 들어갈 사람이 넣는 값 (2026-09-02)
|
||||
--
|
||||
-- 011 로 시행청·담당자 배정 자리를 냈고, 남은 표지 빈칸 둘은 **지어낼 수 없는 값**이라
|
||||
-- 사람이 넣을 칸을 만든다. 값 출처를 프로그램이 정하지 않는다.
|
||||
-- 연도·기번 — 실무문서 폴더명 관행: `2024년 간선임도(기번3-울진.대흥)`
|
||||
-- 사업량 — 표지 「- 사 업 량 :」 칸. 단위·표기가 사업 종류마다 달라 자유 문자열로 받는다.
|
||||
--
|
||||
-- 전부 ADD COLUMN(NULL 허용)이라 기존 행·기존 동작은 그대로다.
|
||||
|
||||
USE aislo_db;
|
||||
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN IF NOT EXISTS project_number VARCHAR(100) NULL COMMENT '표지 연도·기번' AFTER client_org,
|
||||
ADD COLUMN IF NOT EXISTS work_amount VARCHAR(100) NULL COMMENT '표지 사업량' AFTER project_number;
|
||||
Reference in New Issue
Block a user