사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.

통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
taekyoungc
2026-06-01 16:15:15 +09:00
parent 21e7b91871
commit 0f1d414f37
129 changed files with 18068 additions and 1585 deletions

View File

@@ -1,70 +1,482 @@
<?php
$bagMeta = is_array($bagMeta ?? null) ? $bagMeta : [];
$inventoryMap = is_array($inventoryMap ?? null) ? $inventoryMap : [];
$availableBagRows = is_array($availableBagRows ?? null) ? $availableBagRows : [];
$recentIssueRows = is_array($recentIssueRows ?? null) ? $recentIssueRows : [];
$dongCodes = is_array($dongCodes ?? null) ? $dongCodes : [];
$freeDongSet = is_array($freeDongSet ?? null) ? $freeDongSet : [];
$destTypeOptions = is_array($destTypeOptions ?? null) ? $destTypeOptions : ['동사무소', '구청', '기타'];
$defaultDestType = (string) old('bi2_dest_type', (string) ($destTypeOptions[0] ?? '구청'));
$oldItemCodes = old('item_bag_code');
$oldItemQtys = old('item_qty');
$oldItemPacks = old('item_pack');
$oldItemCodes = is_array($oldItemCodes) ? $oldItemCodes : [];
$oldItemQtys = is_array($oldItemQtys) ? $oldItemQtys : [];
$oldItemPacks = is_array($oldItemPacks) ? $oldItemPacks : [];
$initialRows = [];
$oldCount = max(count($oldItemCodes), count($oldItemQtys), count($oldItemPacks));
for ($i = 0; $i < $oldCount; $i++) {
$initialRows[] = [
'code' => trim((string) ($oldItemCodes[$i] ?? '')),
'qty' => max(0, (int) ($oldItemQtys[$i] ?? 0)),
'pack' => (string) ($oldItemPacks[$i] ?? 'sheet'),
];
}
if ($initialRows === []) {
$initialRows[] = ['code' => '', 'qty' => 0, 'pack' => 'sheet'];
}
?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">무료용 불출 처리</span>
</section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
<form action="<?= base_url('bag/issue/store') ?>" method="POST" class="space-y-4">
<div class="border border-gray-300 p-3 mt-2 bg-white">
<form action="<?= base_url('bag/issue/store') ?>" method="POST" class="space-y-3" id="bag-issue-form">
<?= csrf_field() ?>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">연도 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bi2_year" type="number" min="2000" max="2099" value="<?= esc(old('bi2_year', date('Y'))) ?>" required/>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-6 gap-2 text-sm">
<div class="flex items-center gap-2 min-w-0">
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출년도</label>
<input class="border border-gray-300 rounded px-2 py-1 w-24 max-w-full text-right" name="bi2_year" type="number" min="2000" max="2099" value="<?= esc(old('bi2_year', date('Y'))) ?>" required/>
</div>
<div class="flex items-center gap-2 min-w-0">
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">분기</label>
<select class="border border-gray-300 rounded px-2 py-1 w-24 max-w-full" name="bi2_quarter" required>
<option value="1" <?= old('bi2_quarter', (string) ceil((int) date('n') / 3)) === '1' ? 'selected' : '' ?>>1/4</option>
<option value="2" <?= old('bi2_quarter', (string) ceil((int) date('n') / 3)) === '2' ? 'selected' : '' ?>>2/4</option>
<option value="3" <?= old('bi2_quarter', (string) ceil((int) date('n') / 3)) === '3' ? 'selected' : '' ?>>3/4</option>
<option value="4" <?= old('bi2_quarter', (string) ceil((int) date('n') / 3)) === '4' ? 'selected' : '' ?>>4/4</option>
</select>
</div>
<div class="flex items-center gap-2 min-w-0">
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출구분</label>
<select class="border border-gray-300 rounded px-2 py-1 w-28 max-w-full" name="bi2_issue_type" id="bi2_issue_type" required>
<option value="무료용" <?= old('bi2_issue_type', '무료용') === '무료용' ? 'selected' : '' ?>>무료용</option>
<option value="공공용" <?= old('bi2_issue_type') === '공공용' ? 'selected' : '' ?>>공공용</option>
</select>
</div>
<div class="flex items-center gap-2 min-w-0">
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출일</label>
<input class="border border-gray-300 rounded px-2 py-1 w-36 max-w-full" name="bi2_issue_date" type="date" value="<?= esc(old('bi2_issue_date', date('Y-m-d'))) ?>" required/>
</div>
<div class="flex items-center gap-2 min-w-0">
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출처구분</label>
<select class="border border-gray-300 rounded px-2 py-1 w-28 max-w-full" name="bi2_dest_type" id="bi2_dest_type">
<?php foreach ($destTypeOptions as $option): ?>
<option value="<?= esc($option) ?>" <?= $defaultDestType === $option ? 'selected' : '' ?>><?= esc($option) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="flex items-center gap-2 min-w-0 md:col-span-2 xl:col-span-2">
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출처(동)</label>
<select class="border border-gray-300 rounded px-2 py-1 w-full min-w-[20rem]" id="bi2_dest_name" name="bi2_dest_name" required>
<option value="">선택</option>
<?php foreach ($dongCodes as $dong): ?>
<?php $dCode = (string) ($dong->cd_code ?? ''); ?>
<?php $dName = (string) ($dong->cd_name ?? $dCode); ?>
<?php $hasFree = isset($freeDongSet[$dCode]); ?>
<?php $oldDest = (string) old('bi2_dest_name'); ?>
<option
value="<?= esc($dName) ?>"
data-dong-code="<?= esc($dCode) ?>"
data-has-free="<?= $hasFree ? '1' : '0' ?>"
<?= $oldDest === $dName ? 'selected' : '' ?>
>
<?= esc($dName) ?><?= $hasFree ? ' (무료용 가능)' : ' (무료용 없음)' ?>
</option>
<?php endforeach; ?>
</select>
<input type="hidden" name="bi2_dest_dong_code" id="bi2_dest_dong_code" value="<?= esc((string) old('bi2_dest_dong_code')) ?>" />
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">분기 <span class="text-red-500">*</span></label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="bi2_quarter" required>
<option value="">선택</option>
<option value="1" <?= old('bi2_quarter') === '1' ? 'selected' : '' ?>>1</option>
<option value="2" <?= old('bi2_quarter') === '2' ? 'selected' : '' ?>>2</option>
<option value="3" <?= old('bi2_quarter') === '3' ? 'selected' : '' ?>>3</option>
<option value="4" <?= old('bi2_quarter') === '4' ? 'selected' : '' ?>>4</option>
</select>
<div class="border border-gray-300 rounded p-2 bg-gray-50 space-y-2">
<div class="flex flex-wrap items-center gap-2 text-sm">
<label class="font-bold text-gray-700">바코드 스캔</label>
<input
id="barcode_input"
type="text"
class="border border-gray-300 rounded px-2 py-1 w-72"
placeholder="스캐너로 바코드를 입력 후 Enter"
autocomplete="off"
/>
<button type="button" id="add-row-btn" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm hover:bg-white">행 추가</button>
<span class="text-xs text-gray-500">동일 바코드 연속 스캔은 무시됩니다.</span>
</div>
<div class="text-xs text-gray-600">
입고 재고가 있는 봉투/스티커만 불출 가능합니다. 저장 시 포장단위가 낱장으로 환산되어 재고가 차감됩니다.
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">구분 <span class="text-red-500">*</span></label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_issue_type" required>
<option value="">선택</option>
<option value="무료용" <?= old('bi2_issue_type') === '무료용' ? 'selected' : '' ?>>무료용</option>
<option value="공공용" <?= old('bi2_issue_type') === '공공용' ? 'selected' : '' ?>>공공용</option>
</select>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">불출일 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_issue_date" type="date" value="<?= esc(old('bi2_issue_date', date('Y-m-d'))) ?>" required/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">불출처 유형</label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_dest_type" type="text" placeholder="동사무소" value="<?= esc(old('bi2_dest_type')) ?>"/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">불출처명 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bi2_dest_name" type="text" value="<?= esc(old('bi2_dest_name')) ?>" required/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">봉투코드 <span class="text-red-500">*</span></label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bi2_bag_code" required>
<option value="">선택</option>
<?php foreach ($bagCodes as $cd): ?>
<option value="<?= esc($cd->cd_code) ?>" <?= old('bi2_bag_code') === $cd->cd_code ? 'selected' : '' ?>>
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">수량 <span class="text-red-500">*</span></label>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bi2_qty" type="number" min="0" value="<?= esc(old('bi2_qty', '0')) ?>" required/>
<div class="border border-gray-300 overflow-auto">
<table class="w-full data-table text-sm" id="issue-item-table">
<thead>
<tr>
<th class="w-14">No</th>
<th class="w-44">봉투코드</th>
<th>봉투종류</th>
<th class="w-28">수량</th>
<th class="w-28">포장</th>
<th class="w-32">재고(낱장)</th>
<th class="w-36">환산(낱장)</th>
<th class="w-20">작업</th>
</tr>
</thead>
<tbody id="issue-item-body"></tbody>
<tfoot>
<tr class="bg-gray-50 font-semibold">
<td colspan="6" class="text-right pr-2">합계(환산 낱장)</td>
<td class="text-right pr-2" id="sum_sheet_qty">0</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('bag/issue') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">저장</button>
<a href="<?= base_url('bag/issue/cancel') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div>
</form>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-2 mt-2">
<section class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">불출 가능 봉투(현재 재고)</div>
<div class="overflow-auto max-h-[300px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-12">No</th>
<th class="w-28">봉투코드</th>
<th>봉투명</th>
<th class="w-24">재고(낱장)</th>
<th class="w-24">팩당 낱장</th>
<th class="w-24">박스당 낱장</th>
</tr>
</thead>
<tbody>
<?php if ($availableBagRows !== []): ?>
<?php foreach ($availableBagRows as $idx => $row): ?>
<tr>
<td class="text-center"><?= $idx + 1 ?></td>
<td class="text-center"><?= esc((string) ($row['bag_code'] ?? '')) ?></td>
<td class="pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($row['inventory_qty'] ?? 0)) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($row['pack_per_sheet'] ?? 1)) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($row['total_per_box'] ?? 1)) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6" class="text-center text-gray-400 py-4">불출 가능한 재고가 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<section class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">최근 불출 내역</div>
<div class="overflow-auto max-h-[300px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-12">No</th>
<th class="w-24">불출일</th>
<th class="w-20">구분</th>
<th class="w-28">불출처</th>
<th class="w-24">봉투코드</th>
<th>봉투명</th>
<th class="w-24">수량(낱장)</th>
<th class="w-16">상태</th>
</tr>
</thead>
<tbody>
<?php if ($recentIssueRows !== []): ?>
<?php foreach ($recentIssueRows as $idx => $row): ?>
<tr>
<td class="text-center"><?= $idx + 1 ?></td>
<td class="text-center"><?= esc((string) ($row->bi2_issue_date ?? '')) ?></td>
<td class="text-center"><?= esc((string) ($row->bi2_issue_type ?? '')) ?></td>
<td class="pl-2"><?= esc((string) ($row->bi2_dest_name ?? '')) ?></td>
<td class="text-center"><?= esc((string) ($row->bi2_bag_code ?? '')) ?></td>
<td class="pl-2"><?= esc((string) ($row->bi2_bag_name ?? '')) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($row->bi2_qty ?? 0)) ?></td>
<td class="text-center">
<?php if ((string) ($row->bi2_status ?? 'normal') === 'cancelled'): ?>
<span class="text-orange-600">취소</span>
<?php else: ?>
정상
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="text-center text-gray-400 py-4">최근 불출 내역이 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</div>
<script>
(() => {
const bagMeta = <?= json_encode($bagMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const initialRows = <?= json_encode($initialRows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const knownCodes = Object.keys(bagMeta);
const body = document.getElementById('issue-item-body');
const sumSheetQtyEl = document.getElementById('sum_sheet_qty');
const barcodeInput = document.getElementById('barcode_input');
const addRowBtn = document.getElementById('add-row-btn');
const issueTypeEl = document.getElementById('bi2_issue_type');
const destTypeEl = document.getElementById('bi2_dest_type');
const destNameEl = document.getElementById('bi2_dest_name');
const destDongCodeEl = document.getElementById('bi2_dest_dong_code');
const form = document.getElementById('bag-issue-form');
let lastScannedCode = '';
const rows = [];
const createBagTypeOptions = (selectedCode) => {
const opts = ['<option value="">선택</option>'];
knownCodes.forEach((code) => {
const name = bagMeta[code]?.name || code;
opts.push(`<option value="${code}" ${code === selectedCode ? 'selected' : ''}>${code} - ${name}</option>`);
});
return opts.join('');
};
const resolveBagCode = (raw) => {
const src = String(raw || '').trim();
if (src === '') return '';
if (bagMeta[src]) return src;
const noSpace = src.replace(/\s+/g, '');
if (bagMeta[noSpace]) return noSpace;
const compact = noSpace.replace(/[^0-9A-Za-z]/g, '');
if (bagMeta[compact]) return compact;
for (const code of knownCodes) {
if (compact.includes(code)) {
return code;
}
}
return '';
};
const toSheetQty = (code, qty, pack) => {
const n = Math.max(0, parseInt(String(qty || 0), 10) || 0);
const meta = bagMeta[code] || { packPerSheet: 1, totalPerBox: 1 };
if (pack === 'box') return n * Math.max(1, parseInt(meta.totalPerBox, 10) || 1);
if (pack === 'pack') return n * Math.max(1, parseInt(meta.packPerSheet, 10) || 1);
return n;
};
const recompute = () => {
let sum = 0;
rows.forEach((row, idx) => {
const tr = row.tr;
tr.querySelector('.col-no').textContent = String(idx + 1);
const code = row.codeInput.value.trim();
const qty = row.qtyInput.value;
const pack = row.packSelect.value;
const sheetQty = toSheetQty(code, qty, pack);
const invQty = parseInt((bagMeta[code]?.inventoryQty || 0), 10) || 0;
row.invCell.textContent = new Intl.NumberFormat('ko-KR').format(invQty);
row.sheetCell.textContent = new Intl.NumberFormat('ko-KR').format(sheetQty);
if (code && sheetQty > invQty) {
row.sheetCell.classList.add('text-red-600', 'font-semibold');
} else {
row.sheetCell.classList.remove('text-red-600', 'font-semibold');
}
sum += sheetQty;
});
sumSheetQtyEl.textContent = new Intl.NumberFormat('ko-KR').format(sum);
};
const syncCode = (row, code, setAsUserSelection = false) => {
const resolved = resolveBagCode(code);
if (resolved === '') return false;
row.codeInput.value = resolved;
row.typeSelect.value = resolved;
if (setAsUserSelection || row.packSelect.value === '') {
row.packSelect.value = (bagMeta[resolved]?.totalPerBox || 1) > 1 ? 'box' : 'sheet';
}
recompute();
return true;
};
const addRow = (seed = {}) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="text-center col-no"></td>
<td class="px-1 py-1">
<input type="text" name="item_bag_code[]" class="code-input border border-gray-300 rounded px-2 py-1 w-full text-sm" value="" placeholder="봉투코드"/>
</td>
<td class="px-1 py-1">
<select name="item_bag_type[]" class="type-select border border-gray-300 rounded px-2 py-1 w-full text-sm">
${createBagTypeOptions('')}
</select>
</td>
<td class="px-1 py-1">
<input type="number" min="0" step="1" name="item_qty[]" class="qty-input border border-gray-300 rounded px-2 py-1 w-full text-sm text-right" value="0"/>
</td>
<td class="px-1 py-1">
<select name="item_pack[]" class="pack-select border border-gray-300 rounded px-2 py-1 w-full text-sm">
<option value="box">박스</option>
<option value="pack">팩</option>
<option value="sheet">낱장</option>
</select>
</td>
<td class="text-right pr-2 inv-cell">0</td>
<td class="text-right pr-2 sheet-cell">0</td>
<td class="text-center">
<button type="button" class="remove-btn text-red-600 hover:underline text-xs">삭제</button>
</td>
`;
body.appendChild(tr);
const row = {
tr,
codeInput: tr.querySelector('.code-input'),
typeSelect: tr.querySelector('.type-select'),
qtyInput: tr.querySelector('.qty-input'),
packSelect: tr.querySelector('.pack-select'),
invCell: tr.querySelector('.inv-cell'),
sheetCell: tr.querySelector('.sheet-cell'),
removeBtn: tr.querySelector('.remove-btn'),
};
rows.push(row);
row.codeInput.addEventListener('change', () => {
const ok = syncCode(row, row.codeInput.value, true);
if (!ok) {
row.codeInput.value = '';
row.typeSelect.value = '';
alert('입고 재고가 있는 봉투코드만 입력할 수 있습니다.');
}
recompute();
});
row.typeSelect.addEventListener('change', () => {
row.codeInput.value = row.typeSelect.value;
recompute();
});
row.qtyInput.addEventListener('input', recompute);
row.packSelect.addEventListener('change', recompute);
row.removeBtn.addEventListener('click', () => {
if (rows.length <= 1) return;
tr.remove();
const i = rows.indexOf(row);
if (i >= 0) rows.splice(i, 1);
recompute();
});
if (seed.code) {
syncCode(row, seed.code, true);
}
row.qtyInput.value = String(Math.max(0, parseInt(String(seed.qty || 0), 10) || 0));
if (['box', 'pack', 'sheet'].includes(String(seed.pack || ''))) {
row.packSelect.value = String(seed.pack);
}
recompute();
return row;
};
const appendScannedRow = (code) => {
const row = addRow({ code, qty: 1, pack: (bagMeta[code]?.totalPerBox || 1) > 1 ? 'box' : 'sheet' });
row.qtyInput.focus();
row.qtyInput.select();
};
const syncDestDongCode = () => {
const opt = destNameEl.options[destNameEl.selectedIndex];
if (!opt) {
destDongCodeEl.value = '';
return;
}
destDongCodeEl.value = opt.getAttribute('data-dong-code') || '';
};
const updateIssueTypeUi = () => {
const isPublic = issueTypeEl.value === '공공용';
if (isPublic) {
destTypeEl.value = '구청';
}
};
addRowBtn.addEventListener('click', () => addRow({ code: '', qty: 0, pack: 'sheet' }));
barcodeInput.addEventListener('keydown', (event) => {
if (event.key !== 'Enter') return;
event.preventDefault();
const resolved = resolveBagCode(barcodeInput.value);
barcodeInput.value = '';
if (!resolved) {
alert('인식 가능한 봉투코드가 아닙니다.');
return;
}
if (resolved === lastScannedCode) {
return;
}
lastScannedCode = resolved;
appendScannedRow(resolved);
});
destNameEl.addEventListener('change', syncDestDongCode);
issueTypeEl.addEventListener('change', updateIssueTypeUi);
form.addEventListener('submit', (event) => {
syncDestDongCode();
updateIssueTypeUi();
const validRows = rows.filter((row) => {
const code = resolveBagCode(row.codeInput.value);
const qty = parseInt(row.qtyInput.value || '0', 10) || 0;
return code !== '' && qty > 0;
});
if (validRows.length === 0) {
event.preventDefault();
alert('불출 품목을 1건 이상 입력해 주세요.');
return;
}
for (const row of validRows) {
const code = resolveBagCode(row.codeInput.value);
const sheetQty = toSheetQty(code, row.qtyInput.value, row.packSelect.value);
const inv = parseInt((bagMeta[code]?.inventoryQty || 0), 10) || 0;
if (sheetQty > inv) {
event.preventDefault();
alert(`재고 부족: ${code} (재고 ${inv}, 요청 ${sheetQty})`);
return;
}
row.codeInput.value = code;
row.typeSelect.value = code;
}
if (issueTypeEl.value === '무료용') {
const selected = destNameEl.options[destNameEl.selectedIndex];
const hasFree = selected ? selected.getAttribute('data-has-free') === '1' : false;
if (!hasFree) {
event.preventDefault();
alert('무료용 불출은 "무료용 가능" 불출처(동)만 선택할 수 있습니다.');
}
}
});
initialRows.forEach((row) => addRow(row));
if (rows.length > 1 && !initialRows[0]?.code && !initialRows[0]?.qty) {
rows[0].tr.remove();
rows.shift();
}
syncDestDongCode();
updateIssueTypeUi();
recompute();
})();
</script>