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 => {
@@ -1037,3 +1037,11 @@ CPU는 더늘리기는 힘들어보이고 GPU 사용을 위해 부품사용에
## 요구 517 (10-31 · 사용자)
그럼 제주만 받은 데이터는 삭제하고 다른곳을 찾아보자.
## 요구 518 (10-31 · 사용자)
경제림육성단지 다운로드함. 어디에 저장해줘? / (붙여넣음: 공공데이터포털 [개발계정] 산림청_경제림육성단지구역도_GW — REST · 자동승인 · 승인 · 활용기간 2026-10-31 ~ 2028-10-31 · XML · End Point https://apis.data.go.kr/1400000/fsInfoService · 일반 인증키(값 적지 않음) · 상세기능 /ecoFrstyOpenAPI 경제림육성단지 국유림 또는 사유림 단지명을 입력받아 기본정보 제공 · 일일 1000 · 활용목적 웹 사이트 개발 · 활용내용 임도설계자동화프로그램 개발) 이부분 확인해봐.
## 요구 519 (10-31 · 사용자)
같은 데이터 인지 확인해보면 될것같은데. / (붙여넣음: 산사태위험지도 WMS — 재난안전 · 제공기관 산림청 · REST · WMS · 활용 신청 258 · 갱신주기 1년 · 수정일 2025-05-30 · 5등급 · 호출 safemap.go.kr/openapi2/IF_0046_WMS · 레이어 A2SM_SANSATAI · serviceKey · srs · bbox · format · width · height · transparent · 범례 lgdInfo?intId=IF_0046) 이거 확인해봐. 그리고 포털 활용신청은 한거야. 이전 채팅에 일반 인증키가 있잖아.
@@ -96,6 +96,28 @@ def test_서고_사본_원본_지킴():
assert library.used_folders(user) == {"combo@system@-@SS1"}
def test_사전조합_원본_복사본_사본_지킴(tmp_path):
"""통합 점검 — 사전조합(`C:참조`) 복사본 · 복사본의 복사본 = 원본 사본 폴더 지킴(업데이트 수요 · 저장 정리)."""
base = library.library_dir(tmp_path)
for name, ref in (
("precombo@personal@1-3@SC-000001", {"층": "personal", "주인": "1/3", "키": "SC-000001"}),
("precombo@personal@1-3@SC-000002", {"층": "personal", "주인": "1/3", "키": "SC-000002"}),
):
(base / name).mkdir(parents=True)
manifest = {"종류": "precombo", "원본": {"참조": ref}}
(base / name / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
src = "C:참조|personal|1/3|SC-000001"
user = {
"daega_cover": {
"K:aa": {"@copy": {"from": src, "name": "복사"}},
"K:bb": {"@copy": {"from": "K:aa", "name": "복사의 복사"}},
}
}
assert library.used_folders(user) == set() # 폴더 모름 = 옛 꼴
assert library.used_folders(user, tmp_path) == {"precombo@personal@1-3@SC-000001"}
assert library.prune(tmp_path, user) == ["precombo@personal@1-3@SC-000002"]
# ── 내 자료집(개인 층 사전조합 · 임시 storage) ─────────────────────────────
+25 -4
View File
@@ -274,9 +274,30 @@ def test_줄마다_갈래_판(geo):
def test_이_짝_잊기(tmp_path, monkeypatch):
monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer)
user = {"company_id": 7, "user_id": 3, "role": "USER"}
gone = {lk.FORGOT: True}
route.keep(user, {"a||": {"확정": "earth:4"}, "b||": {"확정": "earth:5"}})
hopyo_store.remember(tmp_path / "company", {"c||": {"확정": "x"}}, lk.MEMORY)
assert route.keep(user, {"a||": None}) == "personal"
assert set(route.recalled(user)) == {"b||", "c||"}
route.keep(user, {"c||": None})
assert "c||" in route.recalled(user) # 일반 = 회사 기억 못 지움(남음)
assert route.keep(user, {"a||": gone}) == "personal"
assert route.recalled(user)["a||"] == gone and route.recalled(user)["b||"]["확정"] == "earth:5"
route.keep(user, {"c||": gone}) # 일반 = 회사 기억을 개인 층 표시로 덮음
assert route.recalled(user)["c||"] == gone
def test_잊은_짝_느슨한_기억에도_안_나옴(tmp_path, monkeypatch):
monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer)
user = {"company_id": 7, "user_id": 3, "role": "USER"}
row = {"name": "토사절취", "spec": "A", "unit": "m3"}
other = {"name": "토사절취", "spec": "B", "unit": "m3"}
live = {"확정": "earth:4", "이름": "절토(토사)"}
route.keep(user, {lk.key_of(other): live})
assert (
lk.recall(route.recalled(user), row) == live
) # 규격 달라도 품명 · 단위 같음 = 느슨한 기억
route.keep(user, {lk.key_of(row): {lk.FORGOT: True}})
memory = route.recalled(user)
assert lk.recall(memory, row) is None # 잊음 = 느슨한 기억도 끊김
assert lk.recall(memory, other) == live # 다른 줄은 그대로
third = {"name": "토사절취", "spec": "C", "unit": "m3"}
assert lk.recall(memory, third) is None # 잊은 짝 곁에서는 느슨하게 안 좁힘
route.keep(user, {lk.key_of(row): live}) # 다시 이음 = 풀림
assert lk.recall(route.recalled(user), row) == live