diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts index e16279e8..78cd0ea5 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts @@ -4,7 +4,7 @@ * 고친 로직은 저장 전 줄(`row`)을 같이 보내 메모리에서 셈(`POST /calc`) — 파일에 안 씀 * ========================================================================== */ -import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; import { runCalc, type CalcAnswer, @@ -31,22 +31,25 @@ const SUMS = ["노무비", "재료비", "경비", "계"]; export function buildCalc(host: HTMLElement, ctx: CalcContext): void { const fields = (ctx.row.입력 ?? []).map((spec) => { const name = spec.이름; - let control: HTMLInputElement | HTMLSelectElement; + let control: HTMLElement; if (spec.고르기?.length) { - control = el("select", { className: "m01-logic__input" }); - for (const option of ["", ...spec.고르기.map(String)]) { - control.append(el("option", { text: option, attrs: { value: option } })); - } + const pick = createSelectField({ + options: ["", ...spec.고르기.map(String)].map((o) => ({ value: o, text: o })), + value: ctx.values[name] ?? "", + compact: true, + onChange: (v) => (ctx.values[name] = v), + }); + control = pick.root; } else { - control = el("input", { + const box = el("input", { className: "m01-logic__input", attrs: { type: "text", inputmode: "decimal" }, }); - if (spec.범위) control.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`; + if (spec.범위) box.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`; + box.value = ctx.values[name] ?? ""; + box.addEventListener("input", () => (ctx.values[name] = box.value)); + control = box; } - control.value = ctx.values[name] ?? ""; - control.addEventListener("input", () => (ctx.values[name] = control.value)); - control.addEventListener("change", () => (ctx.values[name] = control.value)); const label = spec.단위 ? `${name} (${spec.단위})` : name; return el("label", { className: "m01-logic__field", diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts b/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts index 51aa9f75..9a19ce1d 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts @@ -7,7 +7,7 @@ * 줄 더하기·지우기 · 요소 고르기처럼 모양이 바뀔 때만 `rerender`. * ========================================================================== */ -import { createButton, el } from "@ui/ui_template_elements"; +import { createButton, createSelectField, el } from "@ui/ui_template_elements"; import type { CalcLine, ElementBrief, @@ -70,14 +70,14 @@ function choice( options: string[], value: string | undefined, onPick: (v: string) => void, -): HTMLSelectElement { - const select = el("select", { className: "m01-logic__input" }); - for (const option of value && !options.includes(value) ? [value, ...options] : options) { - select.append(el("option", { text: option, attrs: { value: option } })); - } - select.value = value ?? options[0]; - select.addEventListener("change", () => onPick(select.value)); - return select; +): HTMLElement { + const all = value && !options.includes(value) ? [value, ...options] : options; + return createSelectField({ + options: all.map((o) => ({ value: o, text: o })), + value: value ?? options[0], + compact: true, + onChange: onPick, + }).root; } function dropButton(onClick: () => void): HTMLButtonElement { diff --git a/M01_MasterData/M01_MasterData_UI_Logic_List.ts b/M01_MasterData/M01_MasterData_UI_Logic_List.ts index 31dc2d58..cec2e154 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_List.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_List.ts @@ -4,7 +4,7 @@ * 거르기는 화면에서(목록은 한 번 받음 · 391 줄 남짓) * ========================================================================== */ -import { el } from "@ui/ui_template_elements"; +import { createInputField, el } from "@ui/ui_template_elements"; import { renderTree, type MakeRow, type TreeNode } from "./M01_MasterData_UI_Tree"; import type { LogicSummary } from "./M01_MasterData_UI_Logic_Api"; import { tx } from "./M01_MasterData_UI_Logic_Text"; @@ -40,10 +40,8 @@ export function buildList(onOpen: (item: ListItem) => void): ListHandle { /** 트리에서 고른 범위 — 원문 · 부문 · 장(비면 전체) */ const pick = { book: "", division: "", chapter: "" }; const tree = el("div", { className: "m01-logic__tree" }); - const search = el("input", { - className: "m01-logic__input", - attrs: { type: "search", placeholder: tx("List_Search") }, - }); + const searchField = createInputField({ type: "search", placeholder: tx("List_Search") }); + const search = searchField.input; const blockedOnly = el("input", { attrs: { type: "checkbox" } }); const count = el("span", { className: "m01-logic__muted" }); const list = el("ul", { className: "m01-logic__list" }); @@ -178,7 +176,7 @@ export function buildList(onOpen: (item: ListItem) => void): ListHandle { el("div", { className: "m01-logic__filters", children: [ - search, + searchField.root, el("label", { className: "m01-logic__check", children: [blockedOnly, el("span", { text: tx("List_BlockedOnly") })], diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts b/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts index 1813070d..5f59798a 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts @@ -4,7 +4,13 @@ * 표 고르기(소요량·계수)는 값칸 단추를 눌러 `찾기(…).칸` 으로 수량에 넣음 * ========================================================================== */ -import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + createSelectField, + el, + showToast, +} from "@ui/ui_template_elements"; import { searchElements, type ElementBrief } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber, type PickDone, type PickMode } from "./M01_MasterData_UI_Logic_Edit"; import { tx } from "./M01_MasterData_UI_Logic_Text"; @@ -16,13 +22,14 @@ const GROUPS: Record = { export function openPicker(mode: PickMode, group: string, done: PickDone): void { const groups = GROUPS[mode]; - const select = el("select", { className: "m01-logic__input" }); - for (const g of groups) select.append(el("option", { text: g, attrs: { value: g } })); - select.value = groups.includes(group) ? group : groups[0]; - const search = el("input", { - className: "m01-logic__input", - attrs: { type: "search", placeholder: tx("Pick_Search") }, + const groupField = createSelectField({ + options: groups.map((g) => ({ value: g, text: g })), + value: groups.includes(group) ? group : groups[0], + compact: true, }); + const select = groupField.select; + const searchField = createInputField({ type: "search", placeholder: tx("Pick_Search") }); + const search = searchField.input; const count = el("span", { className: "m01-logic__muted" }); const list = el("div", { className: "m01-logic__pick-list" }); const close = (): void => backdrop.remove(); @@ -37,7 +44,10 @@ export function openPicker(mode: PickMode, group: string, done: PickDone): void createButton({ label: tx("Pick_Close"), variant: "ghost", onClick: close }), ], }), - el("div", { className: "m01-logic__pick-bar", children: [select, search, count] }), + el("div", { + className: "m01-logic__pick-bar", + children: [groupField.root, searchField.root, count], + }), list, ], }); diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index 87fb5523..c19005e4 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -8,7 +8,13 @@ * ========================================================================== */ import "@ui/ui_template_workflow_layout.css"; -import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + el, + showConfirmDialog, + showToast, +} from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { ROUTES } from "@config/config_frontend"; @@ -42,10 +48,8 @@ function buildPage(): HTMLElement { /* --- 우측: 머리(제목·찾기·저장) + 알림 + 본문 --- */ const title = el("h2", { className: "m01-master__title", text: L("M01_PickFile") }); - const search = el("input", { - className: "m01-master__search", - attrs: { type: "search", placeholder: L("M01_Search") }, - }); + const searchField = createInputField({ type: "search", placeholder: L("M01_Search") }); + const search = searchField.input; const summary = el("span", { className: "m01-master__summary" }); const save = createButton({ label: L("M01_Save"), variant: "filled" }); const drop = createButton({ label: L("M01_Discard"), variant: "ghost" }); @@ -53,7 +57,7 @@ function buildPage(): HTMLElement { const body = el("div", { className: "m01-master__body" }); const head = el("div", { className: "m01-master__head", - children: [title, search, summary, drop, save], + children: [title, searchField.root, summary, drop, save], }); const logicHost = el("div", { className: "m01-master__logic", attrs: { hidden: "" } }); const elementView = el("div", { @@ -98,8 +102,8 @@ function buildPage(): HTMLElement { }, 300); }); - drop.addEventListener("click", () => { - if (!window.confirm(L("M01_DiscardConfirm"))) return; + drop.addEventListener("click", async () => { + if (!(await showConfirmDialog(L("M01_DiscardConfirm")))) return; discard(); showNotice([]); }); diff --git a/M01_MasterData/M01_MasterData_UI_Pick.ts b/M01_MasterData/M01_MasterData_UI_Pick.ts index 17416414..d41767e3 100644 --- a/M01_MasterData/M01_MasterData_UI_Pick.ts +++ b/M01_MasterData/M01_MasterData_UI_Pick.ts @@ -3,7 +3,7 @@ * 고르기 모달 — 가격 연결(자재품목) · 준용 직종을 이름·규격으로 찾아 고르면 ref 를 돌려줌 * ========================================================================== */ -import { el, showToast } from "@ui/ui_template_elements"; +import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { fetchPick, type PickItem, type PickKind } from "./M01_MasterData_Api_Fetch"; import { show } from "./M01_MasterData_UI_Cells"; @@ -28,21 +28,15 @@ export interface PickOptions { cond?: { 이름: string; 규격: string; onCond: (c: Cond) => void }; } -const button = (text: string): HTMLButtonElement => - el("button", { className: "m01-master__row-btn", text, attrs: { type: "button" } }); +const button = (label: string): HTMLButtonElement => createButton({ label, variant: "ghost" }); -const input = (value: string, placeholder: string): HTMLInputElement => { - const box = el("input", { - className: "m01-master__search", - attrs: { type: "search", placeholder }, - }); - box.value = value; - return box; -}; +const field = (value: string, placeholder: string): ReturnType => + createInputField({ type: "search", placeholder, value }); export function openPickModal(opt: PickOptions): void { const close = (): void => back.remove(); - const search = input(opt.seed, L("M01_ProcureSearch")); + const searchField = field(opt.seed, L("M01_ProcureSearch")); + const search = searchField.input; const list = el("div", { className: "m01-procure__list" }); const cut = button(L("M01_ProcureCut")); cut.disabled = !opt.current; @@ -56,8 +50,8 @@ export function openPickModal(opt: PickOptions): void { const condRow: HTMLElement[] = []; if (opt.cond) { const { onCond } = opt.cond; - const name = input(opt.cond.이름, L("M01_PriceCondName")); - const spec = input(opt.cond.규격, L("M01_PriceCondSpec")); + const name = field(opt.cond.이름, L("M01_PriceCondName")).input; + const spec = field(opt.cond.규격, L("M01_PriceCondSpec")).input; const go = button(L("M01_PriceCond")); go.addEventListener("click", () => { if (!name.value.trim()) return; @@ -72,7 +66,7 @@ export function openPickModal(opt: PickOptions): void { children: [ el("h3", { text: opt.title }), el("p", { className: "m01-master__muted", text: opt.current }), - search, + searchField.root, list, ...condRow, el("div", { className: "m01-procure__foot", children: [cut, shut] }), diff --git a/M01_MasterData/M01_MasterData_UI_Rows.ts b/M01_MasterData/M01_MasterData_UI_Rows.ts index b9b676d9..264107bb 100644 --- a/M01_MasterData/M01_MasterData_UI_Rows.ts +++ b/M01_MasterData/M01_MasterData_UI_Rows.ts @@ -250,19 +250,12 @@ export function renderRows( } function actionCell(label: string, onClick: () => void): HTMLTableCellElement { - const button = el("button", { - className: "m01-master__row-btn", - text: label, - attrs: { type: "button" }, - }); - button.addEventListener("click", onClick); - return el("td", { children: [button] }); + return el("td", { children: [createButton({ label, variant: "ghost", onClick })] }); } type Put = (row: Row) => void; -const rowBtn = (text: string): HTMLButtonElement => - el("button", { className: "m01-master__row-btn", text, attrs: { type: "button" } }); +const rowBtn = (label: string): HTMLButtonElement => createButton({ label, variant: "ghost" }); /** 품셈재료 — 가격 연결(자재품목 키 · 후보 조건 · 없음) · 고르기 모달. */ function linkedCell(row: Row, cur: Row, put?: Put): HTMLTableCellElement { diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index 059882fc..d189e998 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -5,7 +5,7 @@ * 펼치면 하위 목록(거름 · 파일) · 환율/요율은 누르면 바로 표 · 로직은 안에 로직 목록 * ========================================================================== */ -import { el, showToast } from "@ui/ui_template_elements"; +import { createSelectField, el, showToast } from "@ui/ui_template_elements"; import { attachCollapsible } from "@ui/ui_template_collapsible"; import { t as L } from "@ui/ui_template_locale"; import { @@ -38,6 +38,8 @@ export interface SideHandle { const LABOR_FILTER = "m01.laborFilter"; const LABOR_ID = "인력|"; const MATERIAL_NAMES: Record = { 나라장터자재: "나라장터", 오피넷유가: "유가" }; +/** 재료 컨테이너 차례 — 여기 없는 파일은 뒤에 */ +const MATERIAL_ORDER = ["자재품목", "오피넷유가", "품셈재료"]; /** 파일 이름 → 목록 글자 — 「소요량_건설품셈_10장_창호…」 → 「건설품셈_10장_창호…」 */ export const fileLabel = (file: string): string => @@ -114,64 +116,59 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si } })(); const pickSub = list.find((k) => k.name === saved[0]); - const choose = (name: string, values: string[], value: string): HTMLSelectElement => { - const select = el("select", { - className: "m01-side__select", - attrs: { "aria-label": name }, - children: [L("M01_All"), ...values].map((v, i) => - el("option", { text: v, attrs: { value: i ? v : "" } }), - ), - }); - select.value = value; - return select; - }; - const sub = choose( - L("M01_LaborSub"), - list.map((k) => k.name), - pickSub?.name ?? "", - ); - const detail = choose(L("M01_LaborDetail"), pickSub?.details ?? [], saved[1] ?? ""); - detail.disabled = !pickSub?.details.length; + const opts = (values: string[]): { value: string; text: string }[] => + [L("M01_All"), ...values].map((v, i) => ({ value: i ? v : "", text: v })); + const sub = createSelectField({ + options: opts(list.map((k) => k.name)), + value: pickSub?.name ?? "", + compact: true, + label: L("M01_LaborSub"), + }); + const detail = createSelectField({ + options: opts(pickSub?.details ?? []), + value: saved[1] ?? "", + compact: true, + label: L("M01_LaborDetail"), + disabled: !pickSub?.details.length, + }); const run = (): void => { try { - sessionStorage.setItem(LABOR_FILTER, JSON.stringify([sub.value, detail.value])); + sessionStorage.setItem( + LABOR_FILTER, + JSON.stringify([sub.select.value, detail.select.value]), + ); } catch { /* 기억 못 해도 거름은 됨 */ } - const label = [sub.value, detail.value].filter(Boolean).join(" · ") || L("M01_All"); + const label = + [sub.select.value, detail.select.value].filter(Boolean).join(" · ") || L("M01_All"); setActive(LABOR_ID); - open(file, sub.value, label, detail.value); + open(file, sub.select.value, label, detail.select.value); }; - sub.addEventListener("change", () => { - const details = list.find((k) => k.name === sub.value)?.details ?? []; - detail.replaceChildren( - ...[L("M01_All"), ...details].map((v, i) => - el("option", { text: v, attrs: { value: i ? v : "" } }), - ), - ); - detail.disabled = details.length === 0; + sub.select.addEventListener("change", () => { + const details = list.find((k) => k.name === sub.select.value)?.details ?? []; + detail.setOptions(opts(details), ""); + detail.select.disabled = details.length === 0; run(); }); - detail.addEventListener("change", run); + detail.select.addEventListener("change", run); const all = make({ id: LABOR_ID, label: L("M01_All"), count: file.rows, run }, "", 0, () => { - sub.value = detail.value = ""; - sub.dispatchEvent(new Event("change")); + sub.select.value = ""; + sub.select.dispatchEvent(new Event("change")); }); return el("div", { children: [ all, - el("label", { - className: "m01-side__filter", - children: [el("span", { text: L("M01_LaborSub") }), sub], - }), - el("label", { - className: "m01-side__filter", - children: [el("span", { text: L("M01_LaborDetail") }), detail], - }), + el("div", { className: "m01-side__filter", children: [sub.root, detail.root] }), ], }); }; + const order = (file: string): number => { + const i = MATERIAL_ORDER.indexOf(fileLabel(file)); + return i < 0 ? MATERIAL_ORDER.length : i; + }; + const drawGroup = (group: Group): void => { const files = lists.get(group) ?? []; const body = bodies.get(group); @@ -231,11 +228,18 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si } body.replaceChildren( ...renderTree( - files.map((f) => { - const name = fileLabel(f.file); - const label = group === "재료" ? (MATERIAL_NAMES[name] ?? name) : name; - return { id: `${group}\n${f.file}`, label, count: f.rows, run: () => open(f, "", label) }; - }), + [...files] + .sort((a, b) => order(a.file) - order(b.file)) + .map((f) => { + const name = fileLabel(f.file); + const label = group === "재료" ? (MATERIAL_NAMES[name] ?? name) : name; + return { + id: `${group}\n${f.file}`, + label, + count: f.rows, + run: () => open(f, "", label), + }; + }), make, ), ); diff --git a/M01_MasterData/M01_MasterData_UI_Style.css b/M01_MasterData/M01_MasterData_UI_Style.css index def7f125..99bcbfbd 100644 --- a/M01_MasterData/M01_MasterData_UI_Style.css +++ b/M01_MasterData/M01_MasterData_UI_Style.css @@ -198,15 +198,10 @@ padding: 2px var(--spacing-4); } -.m01-master__row-btn { +/* 표 칸 안 공용 단추 — 줄 높이에 맞춰 작게 */ +.m01-master__grid .ui-btn { padding: 2px var(--spacing-8); - border: 1px solid var(--color-border); - border-radius: var(--radius-buttons); - background: var(--color-surface); - color: var(--color-text-body); - font: inherit; font-size: var(--text-caption); - cursor: pointer; } .m01-master__pager { @@ -302,12 +297,7 @@ .m01-side__filter { display: flex; - align-items: center; + flex-direction: column; gap: var(--spacing-8); padding: var(--spacing-4) var(--spacing-8); } - -.m01-side__select { - flex: 1; - min-width: 0; -} diff --git a/ui_template/ui_template_elements.ts b/ui_template/ui_template_elements.ts index 9e74c56b..cb8fc767 100644 --- a/ui_template/ui_template_elements.ts +++ b/ui_template/ui_template_elements.ts @@ -19,7 +19,6 @@ export { el } from "./ui_template_elements_base"; /** 요소 생성 + 속성/클래스/자식 일괄 설정 헬퍼 */ - /* ----------------------------------------------------------------------------- * 1. 버튼 (Button) — design.md: Filled Brand / Ghost Outlined / Pill Nav * -------------------------------------------------------------------------- */ @@ -135,11 +134,17 @@ export interface SelectFieldOptions { value?: string; onChange?: (value: string) => void; disabled?: boolean; + /** 좁은 자리(표 칸·좌측 패널)용 작은 여백 */ + compact?: boolean; + /** 라벨이 없을 때 보조기기용 이름 */ + ariaLabel?: string; } export interface SelectFieldHandle { root: HTMLDivElement; select: HTMLSelectElement; + /** 선택지를 다시 채움 — value 가 목록에 없으면 첫 항목 */ + setOptions: (options: { value: string; text: string }[], value?: string) => void; } export function createSelectField(opts: SelectFieldOptions): SelectFieldHandle { @@ -150,19 +155,17 @@ export function createSelectField(opts: SelectFieldOptions): SelectFieldHandle { } const select = el("select", { - className: "ui-select", + className: opts.compact ? "ui-select ui-select--sm" : "ui-select", + attrs: opts.ariaLabel ? { "aria-label": opts.ariaLabel } : {}, }); - for (const opt of opts.options) { - const optEl = el("option", { - attrs: { value: opt.value }, - text: opt.text, - }); - if (opts.value !== undefined && opt.value === opts.value) { - optEl.selected = true; - } - select.append(optEl); - } + const setOptions: SelectFieldHandle["setOptions"] = (options, value) => { + select.replaceChildren( + ...options.map((opt) => el("option", { attrs: { value: opt.value }, text: opt.text })), + ); + if (value !== undefined && options.some((opt) => opt.value === value)) select.value = value; + }; + setOptions(opts.options, opts.value); if (opts.disabled) select.disabled = true; @@ -172,7 +175,7 @@ export function createSelectField(opts: SelectFieldOptions): SelectFieldHandle { root.append(select); - return { root, select }; + return { root, select, setOptions }; } /* ----------------------------------------------------------------------------- @@ -360,4 +363,3 @@ export function createWorkflowShell(opts: WorkflowShellOptions): WorkflowShellHa * 7-1. 라인 차트 (Line Chart) — 시계열 데이터 SVG 렌더링 * 외부 라이브러리 없이 인라인 SVG. 색상은 CSS 클래스 + theme.css 변수 참조. * -------------------------------------------------------------------------- */ - diff --git a/ui_template/ui_template_elements_styles.ts b/ui_template/ui_template_elements_styles.ts index 4e89a9ea..598b4df1 100644 --- a/ui_template/ui_template_elements_styles.ts +++ b/ui_template/ui_template_elements_styles.ts @@ -130,6 +130,10 @@ const BASE_CSS = ` border-color: var(--color-focus-ring); box-shadow: 0 0 0 1px var(--color-focus-ring); } +.ui-select--sm { + padding: 4px 28px 4px 8px; + max-width: 100%; +} .ui-select:disabled { opacity: 0.5; cursor: not-allowed; diff --git a/ui_template/ui_template_locale_m1.ts b/ui_template/ui_template_locale_m1.ts index 3395888b..68fbbd6d 100644 --- a/ui_template/ui_template_locale_m1.ts +++ b/ui_template/ui_template_locale_m1.ts @@ -37,12 +37,9 @@ export const ui_locales_m1 = { "This file changed after you started editing — saving will be refused", ], M01_CheckErrors: ["검사에 걸림", "Validation failed"], - M01_ProcureFind: ["조달 찾기", "Find procurement"], - M01_ProcureTitle: ["조달 자료 찾기", "Find procurement data"], M01_ProcureSearch: ["이름·규격 찾기", "Find by name or spec"], M01_ProcureCut: ["연결 끊기", "Unlink"], M01_ProcureClose: ["닫기", "Close"], - M01_ProcureCol: ["조달 연결", "Procurement link"], M01_PriceFind: ["가격 연결", "Link price"], M01_PriceTitle: ["가격 연결 고르기", "Pick price source"], M01_PriceCol: ["가격 연결", "Price link"],