Merge remote-tracking branch 'origin/sub_laptop_5' into sub_laptop_8

This commit is contained in:
2026-10-31 17:46:53 +09:00
9 changed files with 122 additions and 26 deletions
+31 -12
View File
@@ -340,24 +340,43 @@ def copies(project_root: str | Path) -> list[tuple[Path, dict[str, Any]]]:
return out
def used_folders(user: dict[str, Any]) -> set[str]:
def used_folders(user: dict[str, Any], project_root: str | Path | None = None) -> set[str]:
"""설계내역서 덮개 줄 · 거리이정표 운반 연결(251-5)이 잇는 서고 사본 폴더 +
일위대가 복사본(703-2 `@copy`)의 원본 사본."""
copies = [
str(lines["@copy"].get("from") or "")
for lines in (user.get("daega_cover") or {}).values()
일위대가 복사본(703-2 `@copy`)의 원본 사본 — 복사본의 복사본(`K:`)은 맨 처음 원본까지 ·
사전조합 원본(`C:참조`) = 그 참조를 담은 사본 폴더(`project_root` 줄 때)."""
covers = user.get("daega_cover") or {}
def origin(src: str) -> str:
seen: set[str] = set()
while src.startswith("K:") and src not in seen:
seen.add(src)
src = str(((covers.get(src) or {}).get("@copy") or {}).get("from") or "")
return src
srcs = {
origin(str(lines["@copy"].get("from") or ""))
for lines in covers.values()
if isinstance(lines, dict) and isinstance(lines.get("@copy"), dict)
]
return {
str(ref["lib"])
for row in ref_rows(user)
if isinstance(ref := row.get("ref"), dict) and ref.get("tab") == "dg" and ref.get("lib")
} | {src[2:] for src in copies if src.startswith("L:")}
}
pre = {src[2:] for src in srcs if src.startswith("C:")}
return (
{
str(ref["lib"])
for row in ref_rows(user)
if isinstance(ref := row.get("ref"), dict) and ref.get("tab") == "dg" and ref.get("lib")
}
| {src[2:] for src in srcs if src.startswith("L:")}
| {
folder.name
for folder, manifest in (copies(project_root) if pre and project_root else [])
if refs.ref_id((manifest.get("원본") or {}).get("참조") or {}) in pre
}
)
def prune(project_root: str | Path, user: dict[str, Any]) -> list[str]:
"""[저장] 뒤 — 줄이 안 잇는 사본 폴더를 지움(다시 고르면 새로 들여옴) · 지운 이름."""
keep = used_folders(user)
keep = used_folders(user, project_root)
base = library_dir(project_root)
gone = []
for child in sorted(base.iterdir()) if base.is_dir() else []:
+5 -3
View File
@@ -33,6 +33,7 @@ from M02_MasterTemplete.M02_Hopyo_Match import line_key
MEMORY = "내역맞춤"
TABLE, RECALL = "짝 표", "지난번 이음"
FORGOT = "잊음" # 기억 칸 {잊음: True} = 이 짝 잊기 표시 — 느슨한 기억에서도 안 불러옴 · 다시 이으면 덮임
HEADS = ("earth", "st", "haul")
_TAIL = re.compile(r"^(.+?)\((.*)\)$")
ROCK = ("절토(연암)", "암발파")
@@ -97,12 +98,13 @@ def _loose(key: str) -> str:
def recall(memory: dict[str, dict[str, Any]], row: dict[str, Any]) -> dict[str, Any] | None:
"""기억 한 칸 — 같은 열쇠 · 없으면 품명 · 단위 같고 짝이 하나인 것."""
"""기억 한 칸 — 같은 열쇠 · 없으면 품명 · 단위 같고 짝이 하나인 것 ·
잊은 칸은 없음 취급 + 느슨한 기억에서는 다른 짝으로 셈(잊은 짝 곁에서 하나로 좁히지 않음)."""
key = key_of(row)
if isinstance(memory.get(key), dict):
return memory[key]
return None if memory[key].get(FORGOT) else memory[key]
loose = [v for k, v in memory.items() if isinstance(v, dict) and _loose(k) == _loose(key)]
return loose[0] if len({v.get("확정") for v in loose}) == 1 else None
return loose[0] if len({v.get("확정") for v in loose}) == 1 and loose[0].get("확정") else None
def _targets(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
+1 -1
View File
@@ -210,7 +210,7 @@ async def _demand(root: Path, draft: dict[str, Any] | None = None) -> list[str]:
"""단가 수요 키 — 연결된 사본 + 설계내역서 손 줄 로직(166-2) +
거리이정표 운반 연결(251-5) · draft = 화면 캐시(296 · 서고 사본 = 그 줄이 잇는 것만)."""
user = await asyncio.to_thread(calc_user, root, draft) # 운반 값 사본 먼저(263-2)
keep = None if draft is None else library.used_folders(user)
keep = None if draft is None else library.used_folders(user, root)
keys = await asyncio.to_thread(linked_resource_keys, root, keep)
hand = await asyncio.to_thread(sangeun.hand_resource_keys, user)
hand = [*hand, *added_keys(user.get("daega_cover"))] # 705-2 붙인 줄 이음
+4 -3
View File
@@ -10,7 +10,8 @@
이은 짝 = 맞춤 기억에 씀(회사 층 = 회사 관리자 · 아니면 개인 층)
POST …/link/undo {target, user?} → {user, 알림, payload} — 잇기 풀기(맞춤 기억 그대로)
POST …/link/forget {key, user?} → {잊음, 기억층, 남음} — 이 짝 잊기(불러온 줄 · 이은 줄 키 →
맞춤 열쇠 · 쓸 수 있는 층에서 지움 · 남음 = 못 지운 위 층 기억)
맞춤 열쇠 · 쓸 수 있는 층에 잊음 표시 `{잊음: True}` — 느슨한 기억도 안 불러옴 ·
개인 층 표시가 회사 기억을 덮음 · 남음 = 그래도 살아 있는 기억)
맞춤 기억 = 층 `실무자료/내역맞춤.json`(실무자료 맞춤 꼴) · 읽기 = 회사 + 개인(개인이 이김).
끼워 넣기 = `B09_Estimation_Router`(같은 머리 · 같은 권한) · 화면 = `B09_Estimation_UI_Link`.
"""
@@ -175,6 +176,6 @@ async def forget(
memo = memo or next((r["memo"] for r in board["이은"] if r["key"] == body.key), None)
if not memo:
raise HTTPException(status_code=400, detail="불러온 줄 · 이은 줄이 아님")
layer = await asyncio.to_thread(keep, session, {memo: None})
left = memo in await asyncio.to_thread(recalled, session)
layer = await asyncio.to_thread(keep, session, {memo: {linker.FORGOT: True}})
left = bool((await asyncio.to_thread(recalled, session)).get(memo, {}).get("확정"))
return {"잊음": memo, "기억층": layer, "남음": left}
+1 -1
View File
@@ -164,7 +164,7 @@ export function createBillImport(
// 창 닫고 다시 그림 — 표 높이(fillViewport)가 열린 창 가림막을 아래 자리로 셈
close();
done(got.payload, made);
void openBillLink(user(), done, mode.value); // 707-4 다음: 우리 공종으로 잇기
void openBillLink(user(), done, mode.value, true); // 707-4 다음: 지난번 이음 바로 · 남은 것만 창
const n = got.알림 || {};
const head = `새 줄 ${n.실음} · 붙임 ${n.붙임} · 지움 ${n.지움} · 이음 ${n.이음}${tail} — [저장]해야 남음`;
const off: Row[] = got.어긋남 || [];
+25 -2
View File
@@ -110,16 +110,39 @@ function pickOurs(spots: Row[], line: Row, take: (key: string) => void): void {
});
}
/** 잇기 창 — user = 지금 캐시 · mode = 불러올 때 고른 갈래(없으면 마지막) */
export async function openBillLink(user: Row, done: LinkDone, mode?: string): Promise<void> {
/** 잇기 창 — user = 지금 캐시 · mode = 불러올 때 고른 갈래(없으면 마지막) ·
* fresh = 불러오기 끝에서 부름 — 지난번 이음은 먼저 바로 잇고 남은 것만 창에(남은 0 = 창 없음) */
export async function openBillLink(
user: Row,
done: LinkDone,
mode?: string,
fresh = false,
): Promise<void> {
if (mode === "take" || mode === "price") lastMode = mode;
let cur = user;
let seen: Row;
try {
seen = await post("suggest", { user: cur });
if (fresh && seen.저절로) {
const got = await post("apply", {
pairs: [],
auto: true,
mode: lastMode,
counts: {},
user: cur,
});
done(got.payload, got.user);
cur = got.user;
showToast(
`지난번 이음 ${got.알림.이음 + got.알림.묶음} 줄 바로 이음 — [저장]해야 남음`,
"success",
);
seen = await post("suggest", { user: cur });
}
} catch (error) {
return fail("제안을 못 받음")(error);
}
if (fresh && !seen.남은.length) return; // 남은 줄 없음 — 창 안 뜸
if (!seen.남은.length && !seen.이은.length) return; // 불러온 줄 없음
let busy = false;
const run = (job: () => Promise<Row | void>): void => {