Merge remote-tracking branch 'origin/main_desktop_1' into main_laptop_1
This commit is contained in:
@@ -14,6 +14,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-32);
|
gap: var(--spacing-32);
|
||||||
|
/* ⚠ flex 자식은 기본이 `min-width: auto` 라 **내용보다 안 줄어든다** — 그래서 안쪽
|
||||||
|
격자가 넘치면 페이지 몸통이 통째로 가로로 밀렸다(2026-09-08). 여기서 끊는다. */
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.b03-file__main-layout > * {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.b03-file__control-panel {
|
.b03-file__control-panel {
|
||||||
@@ -52,7 +59,8 @@
|
|||||||
칸을 먼저 나누므로 내용과 무관하게 정확히 1:1:1 이 된다. 좁아지면 칸이 접힌다. */
|
칸을 먼저 나누므로 내용과 무관하게 정확히 1:1:1 이 된다. 좁아지면 칸이 접힌다. */
|
||||||
.b03-file__pick-row {
|
.b03-file__pick-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
/* ⚠ 좁은 폭에서는 200px 도 넘친다 — `min(200px, 100%)` 로 컨테이너까지만 줄인다. */
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-8);
|
gap: var(--spacing-8);
|
||||||
}
|
}
|
||||||
@@ -213,10 +221,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 계획노선 | 지형(LAS) — 컨테이너를 둘로 나눈다(2026-08-31 사용자 지시).
|
/* 계획노선 | 지형(LAS) — 컨테이너를 둘로 나눈다(2026-08-31 사용자 지시).
|
||||||
노선은 카드 5장(shapefile 한 벌), 지형은 4장이라 폭을 5:4 비슷하게 준다. */
|
노선은 카드 5장(shapefile 한 벌), 지형은 4장이라 폭을 5:4 비슷하게 준다.
|
||||||
|
|
||||||
|
⚠ **좁아지면 한 줄로 접는다**(2026-09-08 사용자 지적) — 2열을 고정하면 좁은 폭에서
|
||||||
|
각 열이 카드 하나도 못 담아 내용이 컨테이너 밖으로 넘치고 글자가 안 읽혔다.
|
||||||
|
`auto-fit` + `min(360px, 100%)` 이라, 폭이 모자라면 칸 수가 스스로 1로 줄고
|
||||||
|
**최소폭이 컨테이너를 넘지 않는다**(고정 minmax 는 그 아래에서 넘친다). */
|
||||||
.b03-file__columns {
|
.b03-file__columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
|
||||||
gap: var(--spacing-16);
|
gap: var(--spacing-16);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
@@ -353,9 +366,12 @@
|
|||||||
|
|
||||||
/* 카드가 좁아지면 한 줄에 제목·상태·[선택]이 못 들어가 제목이 글자 단위로 접힌다
|
/* 카드가 좁아지면 한 줄에 제목·상태·[선택]이 못 들어가 제목이 글자 단위로 접힌다
|
||||||
(2026-09-03 실측: 안내 패널을 연 상태에서 카드 폭 210px). 폭이 모자라면 열을 줄인다. */
|
(2026-09-03 실측: 안내 패널을 연 상태에서 카드 폭 210px). 폭이 모자라면 열을 줄인다. */
|
||||||
|
/* ⚠ `minmax(260px, …)` 는 **칸이 260px 보다 좁아질 수 없다**는 뜻이라, 컨테이너가
|
||||||
|
그보다 좁아지면 카드가 밖으로 넘친다(2026-09-08 좁은 폭 실측). `min(260px, 100%)`
|
||||||
|
으로 두면 좁을 때 **컨테이너 폭까지 줄어들어** 넘치지 않는다. */
|
||||||
.b03-file__group-content {
|
.b03-file__group-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
|
||||||
gap: var(--spacing-12);
|
gap: var(--spacing-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,11 +441,19 @@
|
|||||||
글자 단위로 접히므로, 자리가 모자라면 배지·삭제 버튼이 다음 줄로 내려가게 한다. */
|
글자 단위로 접히므로, 자리가 모자라면 배지·삭제 버튼이 다음 줄로 내려가게 한다. */
|
||||||
/* 제목·상태·[선택]이 **한 줄**에 선다(2026-09-03) — 줄바꿈을 허용하면 [선택]이 아래로
|
/* 제목·상태·[선택]이 **한 줄**에 선다(2026-09-03) — 줄바꿈을 허용하면 [선택]이 아래로
|
||||||
내려가 카드가 두 줄이 됐다. 자리가 모자라면 제목이 줄어든다(min-width: 0). */
|
내려가 카드가 두 줄이 됐다. 자리가 모자라면 제목이 줄어든다(min-width: 0). */
|
||||||
|
/* ⚠ 좁은 폭에서 배지·[선택]·[×] 가 밀려 나가지 않게 **줄 자체가 넘치지 않도록** 막는다
|
||||||
|
(2026-09-08). 제목은 이미 말줄임이라, 넘치는 것은 늘 오른쪽 붙박이들이었다. */
|
||||||
.b03-file__card-header {
|
.b03-file__card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: var(--spacing-8);
|
gap: var(--spacing-8);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.b03-file__card-header > * {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.b03-file__card-icon {
|
.b03-file__card-icon {
|
||||||
|
|||||||
@@ -134,6 +134,50 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
|||||||
frameFields: Record<string, string>;
|
frameFields: Record<string, string>;
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
|
// ⚠ CAD 는 iframe 이라 **저쪽이 아무 말도 안 하면 화면이 영원히 「불러오는 중」에 머문다**
|
||||||
|
// (2026-09-09 사용자 보고 — 무한 로딩). 끝을 알리는 것은 `drawing-loaded` ·
|
||||||
|
// `drawing-error` 두 통지뿐이고, 그것이 안 오는 길이 둘 있다.
|
||||||
|
// ① iframe 이 아예 안 뜸 — `dist/` 가 없거나 스크립트가 죽음 ⇒ `ready` 가 안 옴
|
||||||
|
// ② 떴는데 도면을 여는 중에 멈춤 ⇒ `loaded` 도 `error` 도 안 옴
|
||||||
|
// 아래 시계가 그 자리를 끊는다. ⚠ **화면 표시만 끊는다** — 뒤늦게 응답이 오면
|
||||||
|
// 그대로 받아 정상으로 되돌아간다(요청을 취소하지 않는다).
|
||||||
|
const CAD_READY_TIMEOUT_MS = 20000;
|
||||||
|
const CAD_LOAD_TIMEOUT_MS = 15000;
|
||||||
|
let loadWatchdog: number | undefined;
|
||||||
|
|
||||||
|
const failCad = (detail: string): void => {
|
||||||
|
cadHost.dataset.loading = "false";
|
||||||
|
cadHost.dataset.error = detail;
|
||||||
|
showToast(detail, "error");
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopLoadWatchdog = (): void => {
|
||||||
|
if (loadWatchdog === undefined) return;
|
||||||
|
window.clearTimeout(loadWatchdog);
|
||||||
|
loadWatchdog = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startLoadWatchdog = (): void => {
|
||||||
|
stopLoadWatchdog();
|
||||||
|
// 아직 `ready` 를 못 받았으면 iframe 이 뜨기를 기다리는 중이라 더 길게 준다.
|
||||||
|
const wait = cadReady ? CAD_LOAD_TIMEOUT_MS : CAD_READY_TIMEOUT_MS;
|
||||||
|
loadWatchdog = window.setTimeout(() => {
|
||||||
|
loadWatchdog = undefined;
|
||||||
|
if (cadHost.dataset.loading !== "true") return;
|
||||||
|
failCad(
|
||||||
|
cadReady
|
||||||
|
? "CAD 가 도면을 여는 데 너무 오래 걸립니다. 다시 눌러 보세요."
|
||||||
|
: "CAD 화면이 응답하지 않습니다. 새로고침해도 같으면 CAD 빌드(dist)를 확인하세요.",
|
||||||
|
);
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
|
||||||
|
// iframe 자체가 못 뜨는 경우 — 이때는 `ready` 가 영영 안 오므로 기다릴 것 없이 끊는다.
|
||||||
|
frame.addEventListener("error", () => {
|
||||||
|
stopLoadWatchdog();
|
||||||
|
failCad("CAD 화면을 불러오지 못했습니다. CAD 빌드(dist)를 확인하세요.");
|
||||||
|
});
|
||||||
|
|
||||||
let currentDrawing: DesignDrawingItem | undefined;
|
let currentDrawing: DesignDrawingItem | undefined;
|
||||||
let currentIndex = -1;
|
let currentIndex = -1;
|
||||||
let currentConfirmed = false;
|
let currentConfirmed = false;
|
||||||
@@ -261,10 +305,19 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
|||||||
return request;
|
return request;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 목록 전체를 순서대로 미리 받는다. 화면을 막지 않도록 한 번에 하나씩만 간다. */
|
/** 목록 전체를 순서대로 미리 받는다. 화면을 막지 않도록 한 번에 하나씩만 간다.
|
||||||
|
*
|
||||||
|
* ⚠ **사용자가 누른 도면에 자리를 내준다** (2026-09-09 — 무한 로딩으로 보이던 자리).
|
||||||
|
* 도면이 31 장이고 한 장에 0.3~1.0초라, 미리 받기가 도는 동안 클릭하면 그 요청이
|
||||||
|
* **앞의 여러 장 뒤에 줄을 서서** 30초 넘게 안 열렸다. 화면은 「불러오는 중」 그대로였다.
|
||||||
|
* 한 장 받을 때마다 **클릭이 끼어들 틈**을 주고, 클릭이 도는 동안은 쉬어 준다.
|
||||||
|
*/
|
||||||
const prefetchAllDrawings = async (): Promise<void> => {
|
const prefetchAllDrawings = async (): Promise<void> => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
for (const item of drawings) {
|
for (const item of drawings) {
|
||||||
|
// 클릭이 도는 중이면 끝날 때까지 기다린다 — 미리 받기는 급하지 않다.
|
||||||
|
while (loadInFlight) await new Promise((done) => window.setTimeout(done, 120));
|
||||||
|
if (drawingCache.has(item.id)) continue; // 클릭이 이미 받아 둔 장은 건너뛴다
|
||||||
try {
|
try {
|
||||||
await requestDrawing(item);
|
await requestDrawing(item);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -294,6 +347,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
|||||||
if (button) button.dataset.loading = "true";
|
if (button) button.dataset.loading = "true";
|
||||||
cadHost.dataset.loading = "true";
|
cadHost.dataset.loading = "true";
|
||||||
cadHost.dataset.error = ""; // 앞선 실패 표시를 지운다
|
cadHost.dataset.error = ""; // 앞선 실패 표시를 지운다
|
||||||
|
startLoadWatchdog(); // 저쪽이 말이 없으면 여기서 끊는다
|
||||||
try {
|
try {
|
||||||
const response = await requestDrawing(drawing);
|
const response = await requestDrawing(drawing);
|
||||||
currentDrawing = drawing;
|
currentDrawing = drawing;
|
||||||
@@ -304,10 +358,8 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
|||||||
updateInfoPanel(drawing, response);
|
updateInfoPanel(drawing, response);
|
||||||
sendLoad(response.drawing, buildMeta(drawing, response, index));
|
sendLoad(response.drawing, buildMeta(drawing, response, index));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cadHost.dataset.loading = "false";
|
stopLoadWatchdog(); // 여기서 이미 끝났다 — 시계를 두면 늦게 또 오류를 띄운다
|
||||||
cadHost.dataset.error =
|
failCad(error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다.");
|
||||||
error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다.";
|
|
||||||
showToast(cadHost.dataset.error, "error");
|
|
||||||
if (currentDrawing) highlightActive(currentDrawing.id);
|
if (currentDrawing) highlightActive(currentDrawing.id);
|
||||||
} finally {
|
} finally {
|
||||||
loadInFlight = false;
|
loadInFlight = false;
|
||||||
@@ -493,19 +545,22 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
|||||||
);
|
);
|
||||||
} else if (message.type === CAD_READY_MESSAGE) {
|
} else if (message.type === CAD_READY_MESSAGE) {
|
||||||
cadReady = true;
|
cadReady = true;
|
||||||
if (pendingLoad)
|
if (pendingLoad) {
|
||||||
sendLoad(
|
sendLoad(
|
||||||
pendingLoad.drawing,
|
pendingLoad.drawing,
|
||||||
pendingLoad.meta,
|
pendingLoad.meta,
|
||||||
pendingLoad.frameEdit,
|
pendingLoad.frameEdit,
|
||||||
pendingLoad.frameFields,
|
pendingLoad.frameFields,
|
||||||
);
|
);
|
||||||
|
// 기다리던 것이 「iframe 이 뜨기」에서 「도면이 열리기」로 바뀌었다 — 시계를 다시 건다.
|
||||||
|
if (cadHost.dataset.loading === "true") startLoadWatchdog();
|
||||||
|
}
|
||||||
} else if (message.type === CAD_LOADED_MESSAGE) {
|
} else if (message.type === CAD_LOADED_MESSAGE) {
|
||||||
|
stopLoadWatchdog();
|
||||||
cadHost.dataset.loading = "false";
|
cadHost.dataset.loading = "false";
|
||||||
} else if (message.type === CAD_ERROR_MESSAGE) {
|
} else if (message.type === CAD_ERROR_MESSAGE) {
|
||||||
cadHost.dataset.error = message.detail ?? "CAD 도면을 표시하지 못했습니다.";
|
stopLoadWatchdog();
|
||||||
cadHost.dataset.loading = "false";
|
failCad(message.detail ?? "CAD 도면을 표시하지 못했습니다.");
|
||||||
showToast(cadHost.dataset.error, "error");
|
|
||||||
} else if (message.type === CAD_CHANGED_MESSAGE) {
|
} else if (message.type === CAD_CHANGED_MESSAGE) {
|
||||||
// 편집 통지는 **미저장 표시**만 세운다. 확정을 푸는 것은 [수정] 하나뿐이다
|
// 편집 통지는 **미저장 표시**만 세운다. 확정을 푸는 것은 [수정] 하나뿐이다
|
||||||
// (2026-09-01 사용자 확정) — 예전에는 이 통지가 확정을 풀어, 되돌리기나 색
|
// (2026-09-01 사용자 확정) — 예전에는 이 통지가 확정을 풀어, 되돌리기나 색
|
||||||
@@ -551,7 +606,10 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
|||||||
routes: WORKFLOW_STEP_ROUTES,
|
routes: WORKFLOW_STEP_ROUTES,
|
||||||
onStepClick: (stepIndex) => {
|
onStepClick: (stepIndex) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
if (stepIndex > 5 && !allDrawingsConfirmed) {
|
// ⚠ 개발에서는 막지 않는다 (2026-09-09 사용자 지시) — B08·B09 를 확정 전에 봐야
|
||||||
|
// 화면 검증이 되고, 그 우회 단추가 **B08 안에** 있어 여기서 막히면 닿을 길이 없다.
|
||||||
|
// 운영에서는 그대로 막는다.
|
||||||
|
if (stepIndex > 5 && !allDrawingsConfirmed && !import.meta.env.DEV) {
|
||||||
showToast("모든 설계 도면을 확정한 뒤 다음 단계로 이동할 수 있습니다.", "warning");
|
showToast("모든 설계 도면을 확정한 뒤 다음 단계로 이동할 수 있습니다.", "warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,15 @@ export function createStepBar(
|
|||||||
options?.stages?.find((item) => item.stage_no === index) ?? options?.stages?.[index];
|
options?.stages?.find((item) => item.stage_no === index) ?? options?.stages?.[index];
|
||||||
const hasStages = Boolean(options?.stages?.length);
|
const hasStages = Boolean(options?.stages?.length);
|
||||||
const isBlockedState = stage?.state === "NOT_STARTED" || stage?.state === "STALE";
|
const isBlockedState = stage?.state === "NOT_STARTED" || stage?.state === "STALE";
|
||||||
const isEnabled = !hasStages || !isBlockedState || stage?.stage_no === options?.currentStage;
|
// ⚠ 개발 전용 — 막힌 단계도 눌러서 들어간다 (2026-09-09 사용자 지시).
|
||||||
|
// B08 의 「확정 없이 다음으로」 단추가 **B08 화면 안에** 있어서, B07 에서 B08 로 못
|
||||||
|
// 넘어가면 그 단추에 닿을 길이 없었다(닫힌 문 안에 열쇠가 있는 꼴).
|
||||||
|
// ⚠ **잠금을 푸는 것이 아니라 가기만 한다** — 값이 없어 「미확보」로 뜨는 것이 정상이고,
|
||||||
|
// 실제 우회는 B08 의 그 단추가 서버(`common_util_dev_unlock`)로 한다.
|
||||||
|
// ⚠ **서버는 그대로 막혀 있다** — 운영에서는 `ENVIRONMENT` 로 거절한다.
|
||||||
|
const devBypass = import.meta.env.DEV;
|
||||||
|
const isEnabled =
|
||||||
|
devBypass || !hasStages || !isBlockedState || stage?.stage_no === options?.currentStage;
|
||||||
button.classList.toggle("is-enabled", isEnabled);
|
button.classList.toggle("is-enabled", isEnabled);
|
||||||
button.disabled = !isEnabled;
|
button.disabled = !isEnabled;
|
||||||
|
|
||||||
@@ -133,6 +141,13 @@ export function createStepBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 개발 우회로 열어 둔 단계는 **그렇다고 말해 준다** — 조용히 열면 다음 사람이
|
||||||
|
// 「왜 값이 없나」로 헤맨다(B08 의 안내 줄과 같은 뜻).
|
||||||
|
if (devBypass && isBlockedState) {
|
||||||
|
button.classList.add("is-dev-bypass");
|
||||||
|
button.title = `${button.title} (개발: 확정 전이라 값이 비어 보일 수 있습니다)`;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEnabled && options?.routes?.[index]) {
|
if (isEnabled && options?.routes?.[index]) {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
options.onStepClick?.(index, options.routes![index]);
|
options.onStepClick?.(index, options.routes![index]);
|
||||||
|
|||||||
Reference in New Issue
Block a user