feat(est-test): Excel 가져오기 셀부호 밀기·당기기와 설계일자 선택

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 13:05:24 +09:00
co-authored by Claude Opus 5.5
parent 84f9eb4248
commit 785e96809f
7 changed files with 59 additions and 6 deletions
+16
View File
@@ -132,3 +132,19 @@ def test_total_price_without_split_is_rejected_and_replace_mode():
assert result["length"] is True
assert result["first"] == "토공사"
assert result["net"] == 54
def test_shift_column_letters_like_original():
out = run_js(
r"""(() => {
const keys = EstExcelMap.fields.map(([k]) => k);
const right = EstExcelMap.shift({ Code: 'A', Name: 'Q' }, 1);
const left = EstExcelMap.shift({ Code: 'C' }, -1);
let low = ''; try { EstExcelMap.shift({ Code: 'A' }, -1); } catch (e) { low = e.message; }
return { right: keys.map((k) => right[k]).slice(0, 4), last: right[keys.at(-1)], left: left.Code, low };
})()"""
)
assert out["right"] == ["B", "C", "D", "E"]
assert out["last"] == "P"
assert out["left"] == "B"
assert out["low"] == '시작 부호는 "A" 이하로는 할수 없습니다.'
+3
View File
@@ -487,6 +487,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
- 남음: 구성비·요율 전환(CalcBase), 약어 표시(SName), 제비율 기준 문서 보기(원본 한글 파일).
- [x] 수량산출서 검색·전체보기(원본 TFRMMULLANG btnSerch·btnAllDisplay — 빈칸·|·괄호 무시 포함 검색) — 고르기 줄을 `native-quantity-sheets.js` 로 옮겨 700줄 제한 유지.
- 검증완료: `test_est_quantity_sheets.py` 검색 시험 포함 수량 시험 22개 통과. ORCA 브라우저에서 「집수」 검색 → 항목에 집수정이 있는 「배수」만·선택 이동, 전체보기 → 3개 복귀, 샘플 복구.
- [x] Excel 가져오기 셀부호 밀기·당기기(원본 TFRMEXCELTOEST popCellLeft·popupCellRight) — 첫 필드 열만 보고 모든 필드 열을 한 칸 오른쪽(왼쪽)부터 차례로 다시 매김, A 아래는 원본 문구로 거부(웹은 AA 등 두 글자 열도 이어 매김).
- [x] 설계일자 선택(원본 TfrmMonthDate) — 갑지 「날짜 선택」 → 「{년}년 {월}월 일 설계」(일은 빈칸), 견적서 「견적일자 선택」 → 「{년}년 {월}월 {일}일」.
- 검증완료: `test_est_excel_map.py` 4개(밀기 A→B…P, 당기기 C→B, A 거부)·견적서·문서·바인딩 시험 통과. ORCA 브라우저에서 갑지 날짜 2026-03-05 → 「2026년 3월 일 설계」, 저장하지 않고 닫아 복구.
- [x] 연차 차수 중간 삽입(원본 TFRMESTMULTIEDIT popInsert) — 차수 줄의 「위에 삽입」: 고른 차수 앞에 새 차수([기준]) 넣기.
- 검증완료: 바인딩 시험 통과. ORCA 브라우저에서 1차·2차 → 2번째 위 삽입 → 1차·3차·2차, 차수 비워 복구.
- [x] 블럭 파일로 저장하기·블럭 파일에서 붙여넣기(원본 mnuBlockSaveToFile·mnuLoadFromFile) — 복사·잘라낸 행 묶음을 `.estblock.json`(종류 est-web-block-1, 표 이름·행)으로 내려받고, 파일을 읽어 같은 종류 화면에 붙여넣음(다른 공사로 옮기기).
+1 -1
View File
@@ -604,7 +604,7 @@ const EstBusinessActions = (() => {
(typeof EstFind !== "undefined" && EstFind.handle(name, state)) ||
(typeof EstBlank !== "undefined" && EstBlank.handle(name, state)) ||
(typeof EstQuantitySheets !== "undefined" && EstQuantitySheets.handle(name, state, node)) ||
(typeof EstExcelMap !== "undefined" && EstExcelMap.handle(name, state)) ||
(typeof EstExcelMap !== "undefined" && EstExcelMap.handle(name, state, node)) ||
(typeof EstTransfer !== "undefined" && EstTransfer.handle(name, state)) ||
(typeof EstBid !== "undefined" && EstBid.handle(name, state)) ||
(typeof EstFormsLibrary !== "undefined" && EstFormsLibrary.handle(name, state, node)) ||
+18 -1
View File
@@ -130,5 +130,22 @@ const EstDocForms = (() => {
run(name, state, node).catch((error) => panel(EstApp.esc(error.message)));
return true;
}
return { tools, fill, handle };
// 설계일자 선택(TfrmMonthDate 0xda6438): 갑지 「{년}년 {월}월 일 설계」(일은 빈칸) · 견적서 「{년}년 {월}월 {일}일」
function dateText(value, format) {
const [y, m, d] = String(value).split("-").map(Number);
if (!y || !m) return "";
return format === "estimate" ? `${y}년 ${m}월 ${d}일` : `${y}년 ${m}월 일 설계`;
}
if (typeof document !== "undefined" && document.addEventListener)
document.addEventListener("change", (event) => {
const t = event.target;
if (t.dataset?.dateTarget === undefined || !t.value) return;
const target = document.querySelector(`#dialog-content [name="${t.dataset.dateTarget}"]`);
if (!target) return;
target.value = dateText(t.value, t.dataset.dateFormat);
target.dispatchEvent(new Event("input", { bubbles: true }));
target.dispatchEvent(new Event("change", { bubbles: true }));
});
return { tools, fill, dateText, handle };
})();
+1
View File
@@ -137,6 +137,7 @@ const EstDocs = (() => {
input("Position", "공사위치", d.Position) +
input("CompanyName", "발주처", d.CompanyName) +
input("Date", "설계일자", d.Date) +
`<label>날짜 선택<input type="date" data-date-target="doc-Date" data-date-format="design"></label>` +
pos(d.CompanyPos) +
area("Explan", "공사개요", d.Explan) +
area("Cost", "공사금액", d.Cost, 3) +
+1 -1
View File
@@ -171,7 +171,7 @@ const EstEstimatePrint = (() => {
`<label><input name="ep-${k}" type="checkbox" ${s[k] ? "checked" : ""}>${label}</label>`;
EstApp.showDialog(
"견적서 작성",
`<div class="form-fields">${input("Title_Name", "제목")}${input("Project_Name", "공사명(| 줄바꿈)")}${input("Order_name", "발주처")}${input("Esti_Date", "견적일자")}${input("Level", "설계내역 레벨표시(0~9)", "number")}${input("F_PageCount", "첫 페이지 줄수", "number")}${input("L_PageCount", "나머지 페이지 줄수", "number")}<label>품명·규격 정렬<select name="ep-Aligment"><option value="0" ${num(s.Aligment) ? "" : "selected"}>왼쪽</option><option value="1" ${num(s.Aligment) ? "selected" : ""}>가운데</option></select></label>${check("Bigo_Show", "비고란 보기")}${check("Company_Show", "공급자 정보 인쇄")}</div><h4>공급자</h4><div class="form-fields">${input("Reg_Num", "등록번호")}${input("ComPany_Name", "상호")}${input("Ceo_Name", "대표자")}${input("Addr", "사업장주소")}${input("Uptae", "업태")}${input("Jongmog", "종목")}${input("Tel", "전화,(FAX)")}</div><button data-action="estimate-print-preview">미리보기</button> <button data-action="estimate-print-print">인쇄</button> <button data-action="estimate-print-save">공급자정보 저장하기(.einf)</button> <button data-action="estimate-print-load">공급자정보 불러오기</button><iframe id="estimate-print-frame" style="width:100%;height:420px;border:1px solid #ccc;margin-top:8px"></iframe>`,
`<div class="form-fields">${input("Title_Name", "제목")}${input("Project_Name", "공사명(| 줄바꿈)")}${input("Order_name", "발주처")}${input("Esti_Date", "견적일자")}<label>견적일자 선택<input type="date" data-date-target="ep-Esti_Date" data-date-format="estimate"></label>${input("Level", "설계내역 레벨표시(0~9)", "number")}${input("F_PageCount", "첫 페이지 줄수", "number")}${input("L_PageCount", "나머지 페이지 줄수", "number")}<label>품명·규격 정렬<select name="ep-Aligment"><option value="0" ${num(s.Aligment) ? "" : "selected"}>왼쪽</option><option value="1" ${num(s.Aligment) ? "selected" : ""}>가운데</option></select></label>${check("Bigo_Show", "비고란 보기")}${check("Company_Show", "공급자 정보 인쇄")}</div><h4>공급자</h4><div class="form-fields">${input("Reg_Num", "등록번호")}${input("ComPany_Name", "상호")}${input("Ceo_Name", "대표자")}${input("Addr", "사업장주소")}${input("Uptae", "업태")}${input("Jongmog", "종목")}${input("Tel", "전화,(FAX)")}</div><button data-action="estimate-print-preview">미리보기</button> <button data-action="estimate-print-print">인쇄</button> <button data-action="estimate-print-save">공급자정보 저장하기(.einf)</button> <button data-action="estimate-print-load">공급자정보 불러오기</button><iframe id="estimate-print-frame" style="width:100%;height:420px;border:1px solid #ccc;margin-top:8px"></iframe>`,
);
}
function read(state) {
+19 -3
View File
@@ -205,7 +205,7 @@ const EstExcelMap = (() => {
`<option value="${value}" ${map.mode === value ? "selected" : ""}>${label}</option>`;
EstApp.showDialog(
"Excel 내역 열 맞춰 가져오기",
`<div class="form-fields"><label>시트<select name="xm-sheet">${sheets}</select></label><label>시작행<input name="xm-start" type="number" value="${map.start}"></label><label>끝행<input name="xm-end" type="number" value="${map.end}"></label><label>가져오기<select name="xm-mode">${mode("append", "순공사비 위에 추가")}${mode("replace", "순공사비 위 행을 이것으로 바꾸기")}</select></label></div><p>각 항목이 있는 Excel 열 문자(A, B …)를 적으세요. 비우면 가져오지 않습니다. 금액은 단가×수량으로 다시 계산합니다.</p><div class="form-fields">${fields.map(([key, label]) => `<label>${label}<input name="xm-${key}" value="${esc(map.columns[key] || "")}" size="3"></label>`).join("")}</div><button data-action="excel-map-preview">미리보기 새로고침</button> <button data-action="excel-map-apply">가져오기</button><div id="excel-map-preview">${preview(state)}</div>`,
`<div class="form-fields"><label>시트<select name="xm-sheet">${sheets}</select></label><label>시작행<input name="xm-start" type="number" value="${map.start}"></label><label>끝행<input name="xm-end" type="number" value="${map.end}"></label><label>가져오기<select name="xm-mode">${mode("append", "순공사비 위에 추가")}${mode("replace", "순공사비 위 행을 이것으로 바꾸기")}</select></label></div><p>각 항목이 있는 Excel 열 문자(A, B …)를 적으세요. 비우면 가져오지 않습니다. 금액은 단가×수량으로 다시 계산합니다.</p><div class="form-fields">${fields.map(([key, label]) => `<label>${label}<input name="xm-${key}" value="${esc(map.columns[key] || "")}" size="3"></label>`).join("")}</div><button data-action="excel-map-shift" data-step="1">셀부호 우로밀기</button><button data-action="excel-map-shift" data-step="-1">셀부호 좌로 당기기</button> <button data-action="excel-map-preview">미리보기 새로고침</button> <button data-action="excel-map-apply">가져오기</button><div id="excel-map-preview">${preview(state)}</div>`,
);
}
@@ -273,7 +273,23 @@ const EstExcelMap = (() => {
state.notice = `Excel에서 ${built.length}행을 가져왔습니다. 단가 고르기로 기초자료에 연결할 수 있습니다.`;
}
function handle(name, state) {
// 셀부호 밀기·당기기(원본 popCellLeft·popupCellRight 0x109ed40·0x109ed70): 첫 필드 열만 보고
// 모든 필드를 그 다음(앞) 열부터 차례로 다시 매긴다(손으로 고친 열은 사라짐).
function shift(columns, step) {
const first = columnIndex(columns[fields[0][0]] || "A");
if (step < 0 && first <= 0) throw Error('시작 부호는 "A" 이하로는 할수 없습니다.');
const start = first + step;
fields.forEach(([key], i) => (columns[key] = columnName(start + i)));
return columns;
}
function handle(name, state, node) {
if (name === "excel-map-shift") {
read(state);
shift(state.excelMap.columns, Number(node.dataset.step));
form(state);
return true;
}
if (name === "excel-map-open") open(state);
else if (name === "excel-map-preview") {
read(state);
@@ -283,5 +299,5 @@ const EstExcelMap = (() => {
return true;
}
return { fields, columnIndex, columnName, records, rows, insert, load, handle };
return { fields, columnIndex, columnName, records, rows, insert, load, shift, handle };
})();