feat(M02 실무자료): 358-1 후보 묶음 — 이름후보(옛 324 다섯) · 뜻겹침 · 출처 같음 · 책 표시 · 고르기 창 묶음 · 검색 칸 = 품명 낱말 · 일괄 적용(N)
- 서버 줄 맞춤에 이름후보(옛 데스크탑 판과 같음 · 600줄 대조 0 다름) · 후보마다 뜻겹침 · 출처같음(조항 ≥ 0.9) · 책 — 차례 · 무게 · 상태 그대로 - 고르기 창 = 1위 / 출처 같음 / 이름 닮은 로직 / 숫자만 닮음(접힘 · 뜻 안 겹치는 후보 회색 끝) · 같은 이름 책 표시 - 버튼 명사형 — 일괄 적용(N) · 엑셀 보기 · 다른 로직 검색 · 재료비 있는 줄은 다른 자재 검색도 - 시험: match 이름후보 다섯 · 되메우기 · 다웰바 · golden 다섯 안 ∪ 이름후보 · screen 묶음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
장부 짝이 규격 다름(품명|단위 느슨 열쇠)이면 ⑤ 아님 — 꼬리표 「별칭 · 규격 다름」 · 짝은 후보
|
||||
끝 덤만.
|
||||
자재 · 별도계상 = 이름 · 규격 같음 하나면 맞음(체크) · 그 밖 후보.
|
||||
로직 줄 후보 묶음 표시(358-1) = 이름후보(옛 324 다섯 `Match.name_cands`) · 후보마다 뜻겹침 · 출처같음 ·
|
||||
책 — 차례 · 무게 · 상태는 그대로.
|
||||
입력값 출처(칸마다) = 별칭 > 블록(옵션 찾기 값이 규격 읽기와 다름) > 규격(`spec_inputs`) > 자동값.
|
||||
체크 = 처음 제안일 뿐 — 들어감은 [확인하고 사전조합 만들기](다 창)만. 저장 없음 · 셈은 서버 단독.
|
||||
장부 = `M02_Hopyo_Ledger`(다 창) · 확인된 사전조합 = `M02_Hopyo_Build.confirmed`(다 창).
|
||||
@@ -214,15 +216,35 @@ def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, A
|
||||
if alias and alias.get("갈래") in SKIP:
|
||||
return {**out, "상태": "뺌", "뺌": alias["갈래"], "별칭": _alias_info(alias, False)}
|
||||
if alias and alias.get("갈래") == "짝" and alias.get("짝"):
|
||||
return {**out, **_aliased(line, ctx, alias, entry)}
|
||||
if kind in hm.MATERIAL_KINDS:
|
||||
got = _aliased(line, ctx, alias, entry)
|
||||
elif kind in hm.MATERIAL_KINDS:
|
||||
got = _material(line, ctx, full)
|
||||
elif entry and entry["판정"]["글"] not in (fit.NO_NUMBER, "우리 로직 준비 중"):
|
||||
got = _numeric(line, ctx, entry) if full else _brief(entry)
|
||||
else:
|
||||
why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER
|
||||
got = _names_only(line, ctx, why, full)
|
||||
return {**out, **(_loose(got, loose, line, ctx) if loose and full else got)}
|
||||
got = _loose(got, loose, line, ctx) if loose and full else got
|
||||
return {**out, **(_grouped(got, line, ctx) if full and kind not in hm.MATERIAL_KINDS else got)}
|
||||
|
||||
|
||||
def _grouped(got: dict, line: dict, ctx: Ctx) -> dict:
|
||||
"""묶음 표시(358-1) — 후보 · 이름후보(옛 324 다섯)마다 뜻겹침 · 출처같음(조항 ≥ 0.9) · 책 ·
|
||||
차례 · 무게 · 상태 그대로(아무것도 숨기지 않음 · 묶음은 화면 몫)."""
|
||||
mine = hm.words(line.get("이름"))
|
||||
theirs = hm.fix_typo(((line.get("근거") or {}).get("출처")) or [])
|
||||
table = hopyo_basis.ours()
|
||||
|
||||
def mark(c: dict) -> dict:
|
||||
one, our = ctx.names.get(c.get("로직")), table.get(c.get("로직"))
|
||||
if one is None:
|
||||
return c
|
||||
same = bool(theirs and our) and hm.clause(theirs, our)[0] >= 0.9
|
||||
book = (our or {}).get("책", "")
|
||||
return {**c, "뜻겹침": hm.near(mine, one["words"]), "출처같음": same, "책": book}
|
||||
|
||||
named = [mark(c) for c in hm.name_cands(line, ctx.got)]
|
||||
return {**got, "후보": [mark(c) for c in got.get("후보") or []], "이름후보": named}
|
||||
|
||||
|
||||
def _loose(got: dict, alias: dict, line: dict, ctx: Ctx) -> dict:
|
||||
|
||||
@@ -292,10 +292,14 @@ def _exact(line: dict[str, Any], pool: str, got: dict[str, Any]) -> list[dict[st
|
||||
|
||||
|
||||
def candidates(
|
||||
line: dict[str, Any], pool: str, got: dict[str, Any] | None = None, limit: int | None = 5
|
||||
line: dict[str, Any],
|
||||
pool: str,
|
||||
got: dict[str, Any] | None = None,
|
||||
limit: int | None = 5,
|
||||
fold: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""③ 정리 이름 같음 먼저 · ④ 글자 두 개 묶음 점수 + 단위 맞음 + 산림품셈 덤 — 위 `limit`
|
||||
(None = 모두)."""
|
||||
(None = 모두) · `fold` = 로직도 (이름, 규격, 단위)로 접음(옛 324 꼴)."""
|
||||
me = tidy(line.get("이름"))
|
||||
grams, unit = _grams(me), line.get("단위")
|
||||
text = mm.plain_spec(f"{line.get('이름')}{line.get('규격')}")
|
||||
@@ -313,7 +317,8 @@ def candidates(
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
out = []
|
||||
for score, one in scored:
|
||||
label = one.get("로직") or (one["이름"], one.get("규격"), one["단위"]) # 로직은 접지 않음
|
||||
label = (one["이름"], one.get("규격"), one["단위"])
|
||||
label = label if fold else one.get("로직") or label # 로직은 접지 않음(fold 아니면)
|
||||
if label in seen:
|
||||
continue
|
||||
seen.add(label)
|
||||
@@ -323,6 +328,19 @@ def candidates(
|
||||
return out
|
||||
|
||||
|
||||
def name_cands(line: dict[str, Any], got: dict[str, Any] | None = None) -> list[dict[str, Any]]:
|
||||
"""이름 닮은 로직 다섯(358-1 · 옛 324 데스크탑 판 그대로) — 이름 같음 · 씨앗(점수 9) 먼저 +
|
||||
이름 점수 후보(로직도 이름 · 규격 · 단위로 접음) · 숫자 판정 차례와 따로(표시 묶음만)."""
|
||||
got = got or targets()
|
||||
same = [_public(x) for x in _exact(line, "로직", got)]
|
||||
rest = [
|
||||
c
|
||||
for c in candidates(line, "로직", got, 5, fold=True)
|
||||
if {k: v for k, v in c.items() if k != "점수"} not in same
|
||||
]
|
||||
return ([{**x, "점수": 9} for x in same] + rest)[:5]
|
||||
|
||||
|
||||
# ── 인용 조항 · 이름 · 근거 차례(338 · 설계 2-4 B 차례) ─────────────────────
|
||||
_OLD_TEXT = re.compile(r"(20(1\d|2[0-4])|1\d년|2[0-4]년|임도품셈|임도표준품셈|사방품셈)")
|
||||
_TYPO_CLAUSE = re.compile(r"^(\d+)-(\d)(\d)(\d)$") # 13-114 → 13-11-4
|
||||
|
||||
@@ -212,6 +212,17 @@
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* 고르기 창 후보 묶음(358-1) — 뜻 낱말 안 겹치는 「후보」 = 회색 · 숫자만 닮음 = 접힘 */
|
||||
.m02-hopyo__cand.is-weak {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.m02-hopyo__group > summary {
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* 별칭 장부(338) — 위 층 줄 = 회색 읽기만 · 고친 줄 = 왼쪽 띠 */
|
||||
.m02-hopyo-ledger__row.is-upper {
|
||||
color: var(--color-text-muted);
|
||||
|
||||
@@ -92,6 +92,10 @@ export interface HopyoTarget {
|
||||
/** 후보 판정(338) — 맞음 · 다른 점 · 후보 */
|
||||
판정?: "맞음" | "다른 점" | "후보";
|
||||
글?: string;
|
||||
/** 묶음 표시(358-1 · 로직 후보만) — 품명 뜻 낱말 겹침 · 엑셀 출처 조항 같음(≥ 0.9) · 책(산림품셈 · 건설품셈) */
|
||||
뜻겹침?: boolean;
|
||||
출처같음?: boolean;
|
||||
책?: string;
|
||||
}
|
||||
|
||||
/** 맞춤 대상 하나(338 · 로직 여럿 가능) — 배 = 환산 × 몫 · 우리 수량 = 엑셀 수량 × 배 */
|
||||
@@ -124,6 +128,8 @@ export interface HopyoMatch {
|
||||
체크: boolean;
|
||||
대상: HopyoPart[];
|
||||
후보?: HopyoTarget[];
|
||||
/** 이름 닮은 로직 다섯(358-1 · 옛 324 판 그대로 · 숫자 판정 차례와 따로 · 로직 줄만) */
|
||||
이름후보?: HopyoTarget[];
|
||||
글?: string;
|
||||
꼬리표?: string[];
|
||||
품?: { 엑셀: HopyoQty; 우리: HopyoQty; 같음: Record<string, boolean> };
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Choose.ts
|
||||
* 일위대가 실무자료 고르기 창(PLAN 338-3 · 설계 4-2 · 옛 `Hopyo_View.choose` 를 옮김) + 글 도우미.
|
||||
* · 위 = 엑셀 블록 품(직종 · 장비 · 자재) · 인용 조항 · [엑셀에서 보기](단가산출 그 행)
|
||||
* · 후보 다섯 = 이름 · 키 · 판정 꼬리표(맞음 / 다른 점 / 후보) · 글 · 근거 꼬리표(근거 맞음 초록) · 금액 비 없음
|
||||
* · 고름 = [고름](한 로직) · 또는 체크 + 몫 칸 → [고른 n개 함께](조합) · 옵션은 부르는 쪽이 서버 `guess` 로 채움
|
||||
* · [다른 로직 찾기] = `openLogicPicker` 에 찾기 글 미리 넣음(엑셀 인용 조항 번호 → 없으면 품명 첫 뜻 낱말)
|
||||
* 일위대가 실무자료 고르기 창(PLAN 338-3 · 358 · 설계 4-2 · 옛 `Hopyo_View.choose` 를 옮김) + 글 도우미.
|
||||
* · 위 = 엑셀 블록 품(직종 · 장비 · 자재) · 인용 조항 · [엑셀 보기](단가산출 그 행)
|
||||
* · 후보 묶음(358-1 · 숨김 없음) = 1위 + 판정 / 출처 같음 / 이름 닮은 로직(옛 324 다섯) / 숫자만 닮음(접힘 ·
|
||||
* 뜻 낱말 안 겹치는 「후보」는 회색 끝) · 같은 이름이면 책(산림 · 건설) · 판정 꼬리표 · 근거 꼬리표(근거 맞음 초록)
|
||||
* · 고름 = [후보](한 로직) · 또는 체크 + 몫 칸 → [일괄 적용(n)](조합) · 옵션은 부르는 쪽이 서버 `guess` 로 채움
|
||||
* · [다른 로직 검색] = `openLogicPicker` 에 품명 첫 뜻 낱말 · [다른 자재 검색] = 자재 줄 · 재료비 있는 줄(358-2)
|
||||
* 저장 안 함 — 고른 것은 부르는 쪽 캐시(확인 창)로만 감.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -15,6 +16,7 @@ import type {
|
||||
HopyoBasisHit,
|
||||
HopyoLine,
|
||||
HopyoLineBasis,
|
||||
HopyoMatch,
|
||||
HopyoQty,
|
||||
HopyoTarget,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
@@ -95,13 +97,71 @@ export const qtyText = (q: HopyoQty | undefined): string =>
|
||||
].join(" · ")
|
||||
: "";
|
||||
|
||||
/** [다른 로직 찾기] 미리 넣을 글 — 엑셀 인용 조항 번호 → 없으면 품명 괄호 밖 첫 뜻 낱말 */
|
||||
export function searchHint(line: Pick<HopyoLine, "이름" | "근거">): string {
|
||||
const clause = line.근거?.출처?.find((x) => x[1])?.[1];
|
||||
if (clause) return clause;
|
||||
/** [다른 로직 검색] 미리 넣을 글 — 품명 괄호 밖 첫 뜻 낱말(358-1 · 조항 번호는 모든 낱말 맞춤이라 0건 위험) */
|
||||
export function searchHint(line: Pick<HopyoLine, "이름">): string {
|
||||
return /[가-힣A-Za-z]{2,}/.exec(line.이름.replace(/\([^()]*\)/g, " "))?.[0] ?? "";
|
||||
}
|
||||
|
||||
/** 후보 묶음 하나 — 제목 · 접힘(숫자만 닮음) · 후보 */
|
||||
export interface CandGroup {
|
||||
제목: string;
|
||||
접힘: boolean;
|
||||
후보: HopyoTarget[];
|
||||
}
|
||||
|
||||
/** 약한 후보 — 판정 「후보」 + 품명 뜻 낱말 안 겹침(회색 · 묶음 끝) */
|
||||
export const weakCand = (c: HopyoTarget): boolean => c.판정 === "후보" && c.뜻겹침 === false;
|
||||
|
||||
/** 후보 묶음(358-1 · 숨김 없음 · 차례는 서버 그대로) — 1위 + 판정 / 출처 같음 / 이름 닮은 로직(옛 판 다섯) /
|
||||
* 숫자만 닮음(접힘 · 약한 후보 뒤로) · 한 후보는 한 묶음에만 · 자재 줄 = 1위 / 그 밖 후보 */
|
||||
export function candGroups(
|
||||
m: Pick<HopyoMatch, "후보" | "이름후보">,
|
||||
material = false,
|
||||
): CandGroup[] {
|
||||
const all = m.후보 ?? [];
|
||||
const seen = new Set<string>();
|
||||
const take = (list: HopyoTarget[]): HopyoTarget[] =>
|
||||
list.filter((c) => {
|
||||
const id = c.로직 ?? c.자재 ?? c.이름 ?? "";
|
||||
if (seen.has(id)) return false;
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
const top = take(all.slice(0, 1));
|
||||
if (material)
|
||||
return [
|
||||
{ 제목: "1위", 접힘: false, 후보: top },
|
||||
{ 제목: "그 밖 후보", 접힘: false, 후보: take(all.slice(1)) },
|
||||
].filter((g) => g.후보.length);
|
||||
const named = m.이름후보 ?? [];
|
||||
const source = take([...all.slice(1), ...named].filter((c) => c.출처같음));
|
||||
const name = take(named);
|
||||
const rest = take(all.slice(1));
|
||||
rest.sort((a, b) => Number(weakCand(a)) - Number(weakCand(b)));
|
||||
return [
|
||||
{ 제목: "1위", 접힘: false, 후보: top },
|
||||
{ 제목: "출처 같음", 접힘: false, 후보: source },
|
||||
{ 제목: "이름 닮은 로직", 접힘: false, 후보: name },
|
||||
{ 제목: "숫자만 닮음", 접힘: true, 후보: rest },
|
||||
].filter((g) => g.후보.length);
|
||||
}
|
||||
|
||||
/** 같은 이름 후보 — 책(산림 · 건설)을 붙여 가를 이름 */
|
||||
export function sameNames(groups: CandGroup[]): Set<string> {
|
||||
const count = new Map<string, number>();
|
||||
for (const c of groups.flatMap((g) => g.후보))
|
||||
count.set(c.이름 ?? "", (count.get(c.이름 ?? "") ?? 0) + 1);
|
||||
return new Set([...count].filter(([name, n]) => name && n > 1).map(([name]) => name));
|
||||
}
|
||||
|
||||
/** 후보 단추 글 — 「이름 [단위] · 키」 · 같은 이름이면 책 두 글자(산림 · 건설) */
|
||||
export const candText = (c: HopyoTarget, twins: Set<string>): string =>
|
||||
`${targetText(c)} · ${c.로직 ?? c.자재 ?? ""}${twins.has(c.이름 ?? "") && c.책 ? ` · ${c.책.slice(0, 2)}` : ""}`;
|
||||
|
||||
/** 자재 고름 — 자재 · 별도계상 줄 · 재료비 있는 단가산출 줄(358-2 · P.V.C파이프설치 등) */
|
||||
export const canPickMaterial = (line: Pick<HopyoLine, "갈래" | "단가">): boolean =>
|
||||
MATERIAL.has(line.갈래) || !!line.단가?.재료비;
|
||||
|
||||
/** 고른 후보 → 만들기 대상(배 1 · 몫 있으면 배 = 몫) */
|
||||
export const pickedTargets = (picked: { c: HopyoTarget; 몫: number | null }[]): BuildTarget[] =>
|
||||
picked.map(({ c, 몫 }) => ({
|
||||
@@ -113,12 +173,12 @@ export const pickedTargets = (picked: { c: HopyoTarget; 몫: number | null }[]):
|
||||
|
||||
export interface ChooseOptions {
|
||||
line: HopyoLine;
|
||||
/** [엑셀에서 보기] — 단가산출 그 행 */
|
||||
/** [엑셀 보기] — 단가산출 그 행 */
|
||||
openSheet?: (행: [number, number]) => void;
|
||||
done: (targets: BuildTarget[]) => void;
|
||||
}
|
||||
|
||||
/** 고르기 창 — 엑셀 품 · 인용 조항 · 후보 다섯(하나 또는 여럿 · 몫) · 다른 로직 · 자재 찾기 */
|
||||
/** 고르기 창 — 엑셀 품 · 인용 조항 · 후보 묶음(하나 또는 여럿 · 몫) · 다른 로직 · 자재 검색 */
|
||||
export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
const { line } = opt;
|
||||
const material = MATERIAL.has(line.갈래);
|
||||
@@ -132,9 +192,9 @@ export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
opt.done(targets);
|
||||
};
|
||||
const picked = new Map<HopyoTarget, HTMLInputElement>();
|
||||
const together = createButton({ label: "고른 0개 함께", size: "sm", disabled: true });
|
||||
const together = createButton({ label: "일괄 적용(0)", size: "sm", disabled: true });
|
||||
const paint = (): void => {
|
||||
together.querySelector(".ui-btn__label")!.textContent = `고른 ${picked.size}개 함께`;
|
||||
together.querySelector(".ui-btn__label")!.textContent = `일괄 적용(${picked.size})`;
|
||||
together.disabled = picked.size < 2;
|
||||
};
|
||||
together.addEventListener("click", () =>
|
||||
@@ -144,7 +204,9 @@ export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
),
|
||||
),
|
||||
);
|
||||
const list = (line.맞춤.후보 ?? []).map((c) => {
|
||||
const groups = candGroups(line.맞춤, material);
|
||||
const twins = sameNames(groups);
|
||||
const row = (c: HopyoTarget): HTMLElement => {
|
||||
const share = el("input", {
|
||||
className: "ui-input ui-input--sm m02-hopyo__times",
|
||||
attrs: { type: "number", step: "0.05", min: "0", max: "1", placeholder: "몫" },
|
||||
@@ -157,12 +219,12 @@ export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
});
|
||||
const tag = basisTag(c.근거);
|
||||
return el("div", {
|
||||
className: "m02-hopyo__cand",
|
||||
className: `m02-hopyo__cand${weakCand(c) ? " is-weak" : ""}`,
|
||||
attrs: { "data-cand": c.로직 ?? c.자재 ?? "" },
|
||||
children: [
|
||||
box,
|
||||
createButton({
|
||||
label: `${targetText(c)} · ${c.로직 ?? c.자재 ?? ""}`,
|
||||
label: candText(c, twins),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => finish(pickedTargets([{ c, 몫: null }])),
|
||||
@@ -176,6 +238,7 @@ export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
]
|
||||
: []),
|
||||
...(c.글 ? [note(c.글)] : []),
|
||||
...(weakCand(c) ? [note("이름 뜻 안 겹침")] : []),
|
||||
...(tag && c.근거
|
||||
? [
|
||||
el("span", {
|
||||
@@ -188,6 +251,18 @@ export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
share,
|
||||
],
|
||||
});
|
||||
};
|
||||
const sections = groups.map((g) => {
|
||||
const list = el("div", { className: "m02-hopyo__cands", children: g.후보.map(row) });
|
||||
const title = `${g.제목} (${g.후보.length})`;
|
||||
if (!g.접힘)
|
||||
return el("div", { attrs: { "data-group": g.제목 }, children: [note(title), list] });
|
||||
const fold = el("details", {
|
||||
className: "m02-hopyo__group",
|
||||
attrs: { "data-group": g.제목 },
|
||||
children: [el("summary", { text: `${title} — 이름 · 출처 안 닮음 · 숫자 지문만` }), list],
|
||||
});
|
||||
return fold;
|
||||
});
|
||||
const excel = qtyText(line.맞춤.품?.엑셀);
|
||||
const basis = line.근거;
|
||||
@@ -199,7 +274,7 @@ export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
...(basis && opt.openSheet
|
||||
? [
|
||||
createButton({
|
||||
label: "엑셀에서 보기",
|
||||
label: "엑셀 보기",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => opt.openSheet?.(basis.행),
|
||||
@@ -208,23 +283,37 @@ export function openHopyoChoose(opt: ChooseOptions): void {
|
||||
: []),
|
||||
],
|
||||
});
|
||||
const more = createButton({
|
||||
label: material ? "다른 자재 찾기…" : "다른 로직 찾기…",
|
||||
const findMaterial = createButton({
|
||||
label: "다른 자재 검색…",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
close();
|
||||
if (material) pickMaterial((x) => opt.done([{ 자재: x.코드, 배: 1 }]));
|
||||
else
|
||||
openLogicPicker((x) => opt.done([{ 로직: x.키, 입력값: {}, 배: 1 }]), undefined, {
|
||||
query: searchHint(line),
|
||||
});
|
||||
pickMaterial((x) => opt.done([{ 자재: x.코드, 배: 1 }]));
|
||||
},
|
||||
});
|
||||
const findLogic = createButton({
|
||||
label: "다른 로직 검색…",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
close();
|
||||
openLogicPicker((x) => opt.done([{ 로직: x.키, 입력값: {}, 배: 1 }]), undefined, {
|
||||
query: searchHint(line),
|
||||
});
|
||||
},
|
||||
});
|
||||
const more = [
|
||||
...(material ? [] : [findLogic]),
|
||||
...(canPickMaterial(line) ? [findMaterial] : []),
|
||||
];
|
||||
body.append(
|
||||
...(head.childElementCount ? [head] : []),
|
||||
note(list.length ? "후보 — 하나 고름 · 여럿은 체크(몫 칸) 뒤 [함께]" : "가까운 것 없음"),
|
||||
el("div", { className: "m02-hopyo__cands", children: list }),
|
||||
el("div", { className: "m02-hopyo__bar", children: [together, more] }),
|
||||
note(
|
||||
groups.length
|
||||
? "후보 — 하나 고름 · 여럿은 체크(몫 칸) 뒤 [일괄 적용]"
|
||||
: "가까운 것 없음 — 아래 검색",
|
||||
),
|
||||
...sections,
|
||||
el("div", { className: "m02-hopyo__bar", children: [together, ...more] }),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user