사용자 매뉴얼·번호알기·gov-portal 대시보드와 메뉴 동선·수불 리포트를 보강한다.
- 사용자 매뉴얼: league/commonmark 기반 bag/manual(로그인 전용), ManualRenderer + Config\Manual manifest, 콘텐츠 8종, E2E - 번호알기(봉투번호확인): bag/number-lookup, BagNumberLookup, E2E - gov-portal 대시보드 시안(기본/strip)·기본코드관리 화면 - 메뉴 관리: 등록·수정 후 메뉴 화면 유지, 수정 버튼 클릭 시 상단 스크롤 - 수불/분석 리포트(LOT 수불·반품/파기·수급계획·추이) 표시 보강 - .gitignore: docs/ → /docs/ 앵커링(최상위 개발문서만 제외, app/Docs는 추적) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,7 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
||||
<?php endif; ?>
|
||||
<form action="<?= base_url('admin/menus/delete/' . (int) $row->mm_idx) ?>" method="post" class="inline ml-1" onsubmit="return confirm('이 메뉴를 삭제하시겠습니까?');">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="mt_idx" value="<?= $mtIdx ?>"/>
|
||||
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
|
||||
</form>
|
||||
</td>
|
||||
@@ -317,6 +318,10 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
||||
formTitle.textContent = '메뉴 수정';
|
||||
btnSubmit.textContent = '수정';
|
||||
btnCancel.style.display = 'inline-block';
|
||||
// 수정 폼이 보이도록 스크롤 최상단으로 이동
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
const sc = document.querySelector('.main-content-area');
|
||||
if (sc) sc.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ if ($bagName !== '' || $bagCode !== '') {
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
팩·박스·낱장 바코드 또는 LOT 번호(보조: <code class="text-xs">lot_no</code> 파라미터)로 조회합니다.
|
||||
낱장 번호 조회 시 <strong>해당 장(바코드)의 판매·반품</strong>만 표시합니다. 팩·박스·LOT 조회는 해당 단위 이력입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -128,6 +128,8 @@ if ($bagName !== '' || $bagCode !== '') {
|
||||
<div><dt class="text-gray-500 inline">LOT</dt>
|
||||
<dd class="font-mono text-xs break-all"><?= esc($lotLabel) ?></dd></div>
|
||||
<?php endif; ?>
|
||||
<div><dt class="text-gray-500 inline">봉투번호</dt>
|
||||
<dd class="font-mono text-xs break-all"><?= esc($barcode !== '' ? $barcode : '-') ?></dd></div>
|
||||
<?php if ($unit !== ''): ?>
|
||||
<div><dt class="text-gray-500 inline">조회단위</dt>
|
||||
<dd><?= esc($unit) ?></dd></div>
|
||||
|
||||
@@ -4,7 +4,15 @@ $endDate = (string) ($endDate ?? date('Y-m-d'));
|
||||
$ioType = (string) ($ioType ?? 'out');
|
||||
$result = is_array($result ?? null) ? $result : (array) ($result ?? []);
|
||||
$queried = (bool) ($queried ?? false);
|
||||
$exportQuery = (string) ($exportQuery ?? 'search=1');
|
||||
$exportParams = $queried ? array_filter([
|
||||
'search' => '1',
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'io_type' => $ioType,
|
||||
], static fn ($v) => $v !== null && $v !== '') : [];
|
||||
$excelUrl = $exportParams !== []
|
||||
? mgmt_url('reports/returns/export') . '?' . http_build_query($exportParams)
|
||||
: '';
|
||||
|
||||
$fmtKrDate = static function (string $ymd): string {
|
||||
$ts = strtotime($ymd);
|
||||
@@ -22,9 +30,10 @@ $printExtraLines = [
|
||||
|
||||
$typeLabel = static function (string $bsType): string {
|
||||
return match ($bsType) {
|
||||
'return' => '반품',
|
||||
'cancel' => '파기',
|
||||
default => $bsType,
|
||||
'return' => '반품',
|
||||
'dispose' => '파기',
|
||||
'cancel' => '파기',
|
||||
default => $bsType,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -42,6 +51,11 @@ $totalQty = 0;
|
||||
foreach ($result as $row) {
|
||||
$totalQty += (int) ($row->qty ?? 0);
|
||||
}
|
||||
|
||||
$tipPage = "지정판매소 반품·물류 입고분 파기 내역을 기간·입출고 구분으로 조회합니다.\n"
|
||||
. "· 출고: 지정판매소 반품 등록 화면에서 처리된 반품\n"
|
||||
. "· 입고: 물류 창고 입고분 파기 처리 내역\n"
|
||||
. "조회 후 표·엑셀·인쇄에 반영됩니다.";
|
||||
?>
|
||||
<?= view('components/print_header', [
|
||||
'printTitle' => '반품 / 파기 현황',
|
||||
@@ -50,10 +64,13 @@ foreach ($result as $row) {
|
||||
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">반품/파기 현황</span>
|
||||
<span class="text-sm font-bold text-gray-700 inline-flex items-center gap-1">
|
||||
반품/파기 현황
|
||||
<?= view('components/field_tooltip', ['text' => $tipPage, 'placement' => 'below']) ?>
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<?php if ($queried && $exportQuery !== ''): ?>
|
||||
<a href="<?= mgmt_url('reports/returns/export?' . esc($exportQuery, 'attr')) ?>"
|
||||
<?php if ($excelUrl !== ''): ?>
|
||||
<a href="<?= esc($excelUrl, 'attr') ?>" target="_blank" rel="noopener noreferrer"
|
||||
class="bg-green-700 text-white px-3 py-1 rounded-sm text-sm hover:bg-green-800">엑셀저장</a>
|
||||
<?php else: ?>
|
||||
<span class="bg-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm cursor-not-allowed" title="조회 후 이용">엑셀저장</span>
|
||||
@@ -89,9 +106,6 @@ foreach ($result as $row) {
|
||||
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
<strong>입고</strong> = 지정판매소 반품(재고 복귀), <strong>출고</strong> = 판매 취소·파기 처리. 조회 후 표·엑셀·인쇄에 반영됩니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<?php if (! $queried): ?>
|
||||
@@ -138,6 +152,26 @@ foreach ($result as $row) {
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field-tip { position: relative; display: inline-flex; vertical-align: middle; }
|
||||
.field-tip-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 14px; height: 14px; font-size: 10px; font-weight: 700; line-height: 1;
|
||||
color: #6b7280; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 50%;
|
||||
cursor: help; user-select: none;
|
||||
}
|
||||
.field-tip-btn:hover, .field-tip-btn:focus { color: #1d4ed8; border-color: #93c5fd; background: #eff6ff; outline: none; }
|
||||
.field-tip-panel {
|
||||
position: absolute; z-index: 60; left: 50%; transform: translateX(-50%);
|
||||
bottom: calc(100% + 6px); width: max-content; max-width: 280px;
|
||||
padding: 0.35rem 0.5rem; border-radius: 4px;
|
||||
background: #1f2937; color: #f9fafb; font-size: 11px; font-weight: 500; line-height: 1.35;
|
||||
text-align: left; white-space: pre-line; box-shadow: 0 2px 8px rgba(0,0,0,.15);
|
||||
opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .12s, visibility .12s;
|
||||
}
|
||||
.field-tip--below .field-tip-panel { bottom: auto; top: calc(100% + 6px); }
|
||||
.field-tip:hover .field-tip-panel,
|
||||
.field-tip:focus-within .field-tip-panel { opacity: 1; visibility: visible; }
|
||||
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
|
||||
@@ -14,6 +14,17 @@ $fmtKrRef = static function (string $ymd): string {
|
||||
return $ts ? date('Y.m.d', $ts) . ' 현재' : $ymd;
|
||||
};
|
||||
|
||||
/** 툴팁: 의미 + 계산(간단) */
|
||||
$tipPage = "봉투 품목별로 재고가 며칠 버티는지, 언제·얼마나 발주할지 보는 수급·발주 계획표입니다.";
|
||||
$tipLead = "의미: 발주 후 입고까지 걸리는 제작기일(일). 재고 소진 전에 발주하려는 여유.\n계산: 발주예정일 = 기준일 + 소진일수 − 보유일수";
|
||||
$tipStock = "의미: 표에 넣을 현재고·총재고 범위.\n기존=바코드 미등록(수기), 바코드=등록 품목.";
|
||||
$tipSales = "의미: 소진일수에 쓸 판매 속도 범위.\n최근 12개월 순판매(또는 바코드 판매) 월평균.";
|
||||
$tipTotal = "의미: 지금·곧 쓸 수 있는 재고 합계.\n계산: 현재고 + 입고예정량";
|
||||
$tipMonth = "의미: 요즘 한 달 판매 규모(평균).\n최근 12개월 월평균 판매량.";
|
||||
$tipDepl = "의미: 이 판매 속도면 재고가 며칠 남는지.\n계산: (총재고 ÷ 월판매량) × 30";
|
||||
$tipSched = "의미: 발주를 넣기 좋은 날(제작기일 반영).\n계산: 기준일 + 소진일수 − 보유일수. 기한 지남=빨간색·긴급";
|
||||
$tipOrder = "의미: 그 시점에 맞춰 제안하는 추가 발주 장수.\n촉박하거나 발주예정일이 지난 품목만 표시.";
|
||||
|
||||
$printExtraLines = [
|
||||
$fmtKrRef($refDate),
|
||||
'적정재고 보유일수(제작기일): ' . $leadDays . '일',
|
||||
@@ -28,7 +39,10 @@ $printExtraLines = [
|
||||
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">쓰레기봉투 수급 계획</span>
|
||||
<span class="text-sm font-bold text-gray-700 inline-flex items-center gap-1">
|
||||
쓰레기봉투 수급 계획
|
||||
<?= view('components/field_tooltip', ['text' => $tipPage, 'placement' => 'below']) ?>
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">인쇄</button>
|
||||
<a href="<?= base_url('dashboard') ?>" class="border border-gray-400 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">종료</a>
|
||||
@@ -47,14 +61,19 @@ $printExtraLines = [
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap">적정재고 보유일수</label>
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap inline-flex items-center gap-0.5">
|
||||
적정재고 보유일수
|
||||
<?= view('components/field_tooltip', ['text' => $tipLead]) ?>
|
||||
</label>
|
||||
<input type="number" name="lead_days" value="<?= (int) $leadDays ?>" min="1" max="365"
|
||||
class="border border-gray-300 rounded px-2 py-1 w-20 text-right" title="제작기일(발주예정일 산정)"/>
|
||||
<span class="text-blue-700 text-xs">※ 제작기일 <?= (int) $leadDays ?>일 기준으로 발주예정일 산정</span>
|
||||
class="border border-gray-300 rounded px-2 py-1 w-20 text-right"/>
|
||||
</div>
|
||||
|
||||
<fieldset class="flex flex-wrap items-center gap-2 border-0 p-0 m-0">
|
||||
<legend class="font-bold text-gray-700 whitespace-nowrap mr-1">현재고 선택 옵션</legend>
|
||||
<legend class="font-bold text-gray-700 whitespace-nowrap mr-1 inline-flex items-center gap-0.5">
|
||||
현재고 선택 옵션
|
||||
<?= view('components/field_tooltip', ['text' => $tipStock]) ?>
|
||||
</legend>
|
||||
<?php foreach (['all' => 'ALL', 'legacy' => '기존 봉투', 'barcode' => '바코드 봉투'] as $val => $lab): ?>
|
||||
<label class="inline-flex items-center gap-1">
|
||||
<input type="radio" name="stock_scope" value="<?= esc($val) ?>" <?= $stockScope === $val ? 'checked' : '' ?>/>
|
||||
@@ -64,7 +83,10 @@ $printExtraLines = [
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="flex flex-wrap items-center gap-2 border-0 p-0 m-0">
|
||||
<legend class="font-bold text-gray-700 whitespace-nowrap mr-1">월 평균판매량 선택 옵션</legend>
|
||||
<legend class="font-bold text-gray-700 whitespace-nowrap mr-1 inline-flex items-center gap-0.5">
|
||||
월 평균판매량 선택 옵션
|
||||
<?= view('components/field_tooltip', ['text' => $tipSales]) ?>
|
||||
</legend>
|
||||
<?php foreach (['all' => 'ALL', 'legacy' => '기존 봉투', 'barcode' => '바코드 봉투'] as $val => $lab): ?>
|
||||
<label class="inline-flex items-center gap-1">
|
||||
<input type="radio" name="sales_scope" value="<?= esc($val) ?>" <?= $salesScope === $val ? 'checked' : '' ?>/>
|
||||
@@ -75,12 +97,6 @@ $printExtraLines = [
|
||||
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-2 max-w-4xl">
|
||||
<strong>기존 봉투</strong> = 입고 팩 바코드 미등록 품목(수기 재고),
|
||||
<strong>바코드 봉투</strong> = <code class="text-xs">bag_receiving_pack_code</code> 등록 품목.
|
||||
월판매량은 최근 12개월 순판매(또는 바코드 판매 스캔)의 월평균입니다.
|
||||
소진일수 = (총재고÷월판매량)×30, 발주예정일 = 기준일+소진일수−보유일수, 과거일은 빨간색.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<?php if (! $queried): ?>
|
||||
@@ -105,11 +121,21 @@ $printExtraLines = [
|
||||
<th class="sp-col-num text-right">발주시재고</th>
|
||||
<th class="sp-col-num text-right border-l">현재고</th>
|
||||
<th class="sp-col-num text-right">입고예정량</th>
|
||||
<th class="sp-col-num text-right">총재고</th>
|
||||
<th class="sp-col-num text-right">월판매량</th>
|
||||
<th class="sp-col-num text-right">소진일수(일)</th>
|
||||
<th class="sp-col-date text-center border-l">발주예정일</th>
|
||||
<th class="sp-col-num text-right">발주수량</th>
|
||||
<th class="sp-col-num text-right">
|
||||
<span class="inline-flex items-center justify-end gap-0.5 w-full">총재고<?= view('components/field_tooltip', ['text' => $tipTotal, 'placement' => 'below']) ?></span>
|
||||
</th>
|
||||
<th class="sp-col-num text-right">
|
||||
<span class="inline-flex items-center justify-end gap-0.5 w-full">월판매량<?= view('components/field_tooltip', ['text' => $tipMonth, 'placement' => 'below']) ?></span>
|
||||
</th>
|
||||
<th class="sp-col-num text-right">
|
||||
<span class="inline-flex items-center justify-end gap-0.5 w-full">소진일수(일)<?= view('components/field_tooltip', ['text' => $tipDepl, 'placement' => 'below']) ?></span>
|
||||
</th>
|
||||
<th class="sp-col-date text-center border-l">
|
||||
<span class="inline-flex items-center justify-center gap-0.5">발주예정일<?= view('components/field_tooltip', ['text' => $tipSched, 'placement' => 'below']) ?></span>
|
||||
</th>
|
||||
<th class="sp-col-num text-right">
|
||||
<span class="inline-flex items-center justify-end gap-0.5 w-full">발주수량<?= view('components/field_tooltip', ['text' => $tipOrder, 'placement' => 'below']) ?></span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -154,7 +180,28 @@ $printExtraLines = [
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.supply-plan-table thead th { font-size: 0.75rem; line-height: 1.2; padding: 0.35rem 0.25rem; }
|
||||
.field-tip { position: relative; display: inline-flex; vertical-align: middle; }
|
||||
.field-tip-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 14px; height: 14px; font-size: 10px; font-weight: 700; line-height: 1;
|
||||
color: #6b7280; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 50%;
|
||||
cursor: help; user-select: none;
|
||||
}
|
||||
.field-tip-btn:hover, .field-tip-btn:focus { color: #1d4ed8; border-color: #93c5fd; background: #eff6ff; outline: none; }
|
||||
.field-tip-panel {
|
||||
position: absolute; z-index: 60; left: 50%; transform: translateX(-50%);
|
||||
bottom: calc(100% + 6px); width: max-content; max-width: 280px;
|
||||
padding: 0.35rem 0.5rem; border-radius: 4px;
|
||||
background: #1f2937; color: #f9fafb; font-size: 11px; font-weight: 500; line-height: 1.35;
|
||||
text-align: left; white-space: pre-line; box-shadow: 0 2px 8px rgba(0,0,0,.15);
|
||||
opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .12s, visibility .12s;
|
||||
}
|
||||
.field-tip--below .field-tip-panel { bottom: auto; top: calc(100% + 6px); }
|
||||
.field-tip:hover .field-tip-panel,
|
||||
.field-tip:focus-within .field-tip-panel { opacity: 1; visibility: visible; }
|
||||
|
||||
.supply-plan-table thead th { font-size: 0.75rem; line-height: 1.2; padding: 0.35rem 0.25rem; overflow: visible; }
|
||||
.supply-plan-table thead th .field-tip-panel { max-width: 260px; }
|
||||
.supply-plan-table tbody td { padding: 0.25rem 0.35rem; }
|
||||
|
||||
@media screen {
|
||||
|
||||
168
app/Views/bag/_dev_all_sales_panel.php
Normal file
168
app/Views/bag/_dev_all_sales_panel.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/**
|
||||
* [개발용 임시] 판매관리·전화접수 화면 하단에 끼우는 "전체 처리 흐름" 표.
|
||||
*
|
||||
* - 통합 데이터 출처:
|
||||
* · shop_order + shop_order_item (단계: order) — 전화/일반 주문 접수
|
||||
* · bag_sale_scan_code state=sold (단계: sale) — 지정판매소 판매 처리
|
||||
* · bag_sale_scan_code state=in_stock (단계: returned) — 반품으로 재고 복귀
|
||||
* - 호출 API: GET /bag/sale/dev-all-sales-history
|
||||
* - 운영 배포 시 본 파일과 라우트/컨트롤러(`Bag::devAllSalesHistory`) 함께 제거.
|
||||
*
|
||||
* 다수 페이지에 동일하게 include 되므로 ID 충돌 회피를 위해 dev-all-sales- 접두어 사용.
|
||||
*/
|
||||
?>
|
||||
<section id="dev-all-sales-panel" class="border border-amber-400 bg-amber-50/50 p-3 mt-3 rounded-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
|
||||
<p class="text-xs text-amber-900 leading-relaxed m-0">
|
||||
<strong class="text-amber-950">[개발용 임시 — 전체 처리 흐름]</strong>
|
||||
현재 지자체의 <strong>주문 접수(order) · 판매 처리(sale) · 반품 복귀(returned)</strong>를 모두 모아
|
||||
<strong>일시 역순 최근 500건</strong>으로 표시합니다. (일시는 DB UTC 기준을 <strong>한국 표준시(KST)</strong>로 변환)
|
||||
운영 배포 시 이 블록을 제거해 주세요.
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span id="dev-all-sales-count" class="text-[13px] text-amber-900"></span>
|
||||
<button type="button" id="dev-all-sales-refresh" class="text-[13px] border border-amber-500 text-amber-800 bg-white rounded px-2 py-0.5 hover:bg-amber-100">새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-h-80 overflow-auto border border-amber-300 bg-white">
|
||||
<table class="w-full data-table text-xs">
|
||||
<thead class="sticky top-0 bg-amber-100 z-10">
|
||||
<tr>
|
||||
<th class="w-40">일시</th>
|
||||
<th class="w-24">단계</th>
|
||||
<th>지정판매소</th>
|
||||
<th class="w-14">주문</th>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-44">봉투 바코드</th>
|
||||
<th class="w-14">포장</th>
|
||||
<th class="w-14">수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dev-all-sales-tbody">
|
||||
<tr><td colspan="8" class="text-center text-gray-400 py-4">불러오는 중…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// base_url() 은 .env 의 app.baseURL 기준 절대 URL을 만든다. 사용자가 다른 host(예: localhost)로
|
||||
// 접속한 경우 cross-origin 으로 가 세션 쿠키가 빠진다. 페이지 origin과 동일하도록 path 부분만 사용.
|
||||
const apiPath = '<?= parse_url(base_url('bag/sale/dev-all-sales-history'), PHP_URL_PATH) ?>';
|
||||
const api = apiPath || '/bag/sale/dev-all-sales-history';
|
||||
const tbody = document.getElementById('dev-all-sales-tbody');
|
||||
const countEl = document.getElementById('dev-all-sales-count');
|
||||
const refreshBtn = document.getElementById('dev-all-sales-refresh');
|
||||
if (!tbody) return;
|
||||
|
||||
const nf = new Intl.NumberFormat('ko-KR');
|
||||
const escapeHtml = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
}[c]));
|
||||
|
||||
function stageBadge(type) {
|
||||
const t = String(type || '').toLowerCase();
|
||||
if (t === 'order') {
|
||||
return '<span class="text-[12px] px-1.5 py-0.5 rounded bg-sky-100 text-sky-800">주문 접수</span>';
|
||||
}
|
||||
if (t === 'sale') {
|
||||
return '<span class="text-[12px] px-1.5 py-0.5 rounded bg-rose-100 text-rose-800">판매</span>';
|
||||
}
|
||||
if (t === 'returned') {
|
||||
return '<span class="text-[12px] px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-800">반품 복귀</span>';
|
||||
}
|
||||
return `<span class="text-[12px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-700">${escapeHtml(type)}</span>`;
|
||||
}
|
||||
|
||||
function rowClass(type) {
|
||||
const t = String(type || '').toLowerCase();
|
||||
if (t === 'returned') return 'text-gray-500';
|
||||
return '';
|
||||
}
|
||||
|
||||
function dash(s) {
|
||||
return (s == null || String(s) === '') ? '<span class="text-gray-300">-</span>' : escapeHtml(s);
|
||||
}
|
||||
|
||||
function renderRows(rows) {
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">처리 내역이 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
const html = rows.map((r) => {
|
||||
const shopText = (r.ds_name && String(r.ds_name).trim() !== '')
|
||||
? `${escapeHtml(r.ds_shop_no || '')} ${escapeHtml(r.ds_name)}`.trim()
|
||||
: `판매소#${escapeHtml(r.ds_idx || '0')}`;
|
||||
const bagText = (r.bag_code || r.bag_name)
|
||||
? `${escapeHtml(r.bag_code || '')} ${escapeHtml(r.bag_name || '')}`.trim()
|
||||
: '<span class="text-gray-300">-</span>';
|
||||
return `
|
||||
<tr class="${rowClass(r.event_type)}">
|
||||
<td class="text-center whitespace-nowrap">${escapeHtml(r.event_time || '')}</td>
|
||||
<td class="text-center">${stageBadge(r.event_type)}</td>
|
||||
<td class="text-left pl-1">${shopText}</td>
|
||||
<td class="text-center">${escapeHtml(r.so_idx || '')}</td>
|
||||
<td class="text-left pl-1">${bagText}</td>
|
||||
<td class="text-center font-mono">${dash(r.code)}</td>
|
||||
<td class="text-center">${dash(r.unit)}</td>
|
||||
<td class="text-right pr-1 tabular-nums">${Number(r.qty || 0) ? nf.format(Number(r.qty)) : '<span class="text-gray-300">-</span>'}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
tbody.innerHTML = html;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">불러오는 중…</td></tr>';
|
||||
countEl.textContent = '';
|
||||
try {
|
||||
const url = api + (api.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now();
|
||||
const res = await fetch(url, {
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store',
|
||||
headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data || data.ok === false) {
|
||||
const msg = (data && data.message) ? data.message : '내역을 불러오지 못했습니다.';
|
||||
tbody.innerHTML = `<tr><td colspan="8" class="text-center text-red-500 py-4">${escapeHtml(msg)}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
// ok=false (예: 지자체 미선택)일 때는 진단을 표 자리에 띄운다.
|
||||
if (data.ok === false) {
|
||||
const sess = data.session || {};
|
||||
const lines = [
|
||||
(data.message || '지자체를 선택해 주세요.'),
|
||||
`세션: mb_idx=${sess.mb_idx ?? '-'} · mb_level=${sess.mb_level ?? '-'} · admin_selected_lg_idx=${sess.admin_selected_lg_idx ?? '-'} · mb_lg_idx=${sess.mb_lg_idx ?? '-'}`,
|
||||
];
|
||||
tbody.innerHTML = `<tr><td colspan="8" class="text-left px-2 py-4 text-amber-900"><div class="mb-1">${escapeHtml(lines[0])}</div><div class="text-[12px] text-gray-500 font-mono">${escapeHtml(lines[1])}</div></td></tr>`;
|
||||
countEl.textContent = `lg_idx=- · 주문 0 / 판매 0 / 반품복귀 0 · 표시 0건`;
|
||||
return;
|
||||
}
|
||||
|
||||
renderRows(data.rows || []);
|
||||
const orders = Number(data.orders ?? 0);
|
||||
const sold = Number(data.sold ?? 0);
|
||||
const ret = Number(data.returned ?? 0);
|
||||
const shown = Array.isArray(data.rows) ? data.rows.length : 0;
|
||||
const lg = data.lg_idx == null ? '-' : data.lg_idx;
|
||||
const sess = data.session || {};
|
||||
const sessText = `[mb_level=${sess.mb_level ?? '-'}, admin=${sess.admin_selected_lg_idx ?? '-'}, mb_lg=${sess.mb_lg_idx ?? '-'}]`;
|
||||
countEl.textContent = `lg_idx=${lg} ${sessText} · 주문 ${nf.format(orders)} / 판매 ${nf.format(sold)} / 반품복귀 ${nf.format(ret)} · 표시 ${nf.format(shown)}건`;
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-red-500 py-4">통신 오류로 불러오지 못했습니다.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
refreshBtn?.addEventListener('click', () => load());
|
||||
|
||||
// 폼 제출(주문 저장·판매 저장 등) 직후 redirect 되어 새로 로드된 경우에도
|
||||
// 항상 최신 상태를 보장한다. pageshow(BFCache 복원 포함)에서 한 번 더 호출.
|
||||
window.addEventListener('pageshow', (ev) => {
|
||||
if (ev.persisted) load();
|
||||
});
|
||||
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
@@ -61,7 +61,7 @@ $prevAvgLabel = ($trendBasis ?? '') === 'month' ? '전년 동월' : '전년 평
|
||||
</div>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-1">(단위: 매) · <?= esc($prevAvgLabel) ?> 대비 기준월 판매량 편차를 표시합니다.</p>
|
||||
<p class="text-xs text-gray-500 mt-1">(단위: 매) · <strong>판매(sale)</strong> 수량만 집계합니다(반품·취소 제외). <?= esc($prevAvgLabel) ?> 대비 기준월 편차를 표시합니다.</p>
|
||||
</section>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
|
||||
@@ -54,7 +54,7 @@ $seasonScope = $seasonMonthsLabel !== ''
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
(단위: 매) · 계절을 바꾸면 자동 조회됩니다.
|
||||
<?php if ($queried && $seasonMonthsLabel !== ''): ?>
|
||||
· 현재: <strong><?= esc($seasonScope) ?></strong> 판매 월평균(3개월 합÷3) vs 전년 동일 계절
|
||||
· 현재: <strong><?= esc($seasonScope) ?></strong> <strong>판매(sale)</strong> 월평균(계절 3개월 합÷3, 반품·취소 제외) vs 전년 동일 계절
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -38,6 +38,7 @@ $printExtra = [
|
||||
|
||||
<div class="m-2 border border-gray-300 overflow-auto">
|
||||
<p class="text-center text-sm font-bold py-2 bg-gray-50 border-b">전년대비 판매 통계분석</p>
|
||||
<p class="text-xs text-gray-500 px-2 py-1 border-b">(단위: 매, 원) · <strong>판매(sale)</strong> 수량·금액만 집계합니다(반품·취소 제외). 증감은 전년·당해 판매량(또는 금액) 차이입니다.</p>
|
||||
<table class="w-full data-table text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -9,8 +9,19 @@ $rows = is_array($rows ?? null) ? $rows : [];
|
||||
$bagProducts = is_array($bagProducts ?? null) ? $bagProducts : [];
|
||||
$bagKindOptions = is_array($bagKindOptions ?? null) ? $bagKindOptions : [];
|
||||
$agencies = is_array($agencies ?? null) ? $agencies : [];
|
||||
$exportQuery = (string) ($exportQuery ?? 'search=1');
|
||||
$queried = (bool) ($queried ?? false);
|
||||
$exportParams = array_filter([
|
||||
'search' => '1',
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'agg_mode' => $aggMode,
|
||||
'bag_code' => $bagCode,
|
||||
'bag_kind' => $bagKind,
|
||||
'sa_idx' => $saIdx > 0 ? (string) $saIdx : '',
|
||||
], static fn ($v) => $v !== null && $v !== '');
|
||||
$excelUrl = $queried
|
||||
? base_url('bag/flow/export') . '?' . http_build_query($exportParams)
|
||||
: '';
|
||||
$fmt = static fn ($n): string => number_format((int) $n);
|
||||
|
||||
$printExtraLines = [];
|
||||
@@ -18,6 +29,13 @@ if ($queried) {
|
||||
$aggLabel = $aggMode === 'daily' ? '일자별' : '기간별';
|
||||
$printExtraLines[] = '조회기간: ' . $startDate . ' ~ ' . $endDate . ' (' . $aggLabel . ')';
|
||||
}
|
||||
|
||||
$tipPage = "조회 기간 동안 봉투 품목별 입고·출고·잔량을 집계하는 수불표입니다.\n"
|
||||
. "· 집계방식: 일자별(날짜마다) / 기간별(기간 합계)\n"
|
||||
. "· 전일재고: 조회 시작일 전날 기준 재고(입고·반품·기타 − 출고 누적)\n"
|
||||
. "· 입고: 입고·반품·기타 / 출고: 판매·일반·무료불출·반품·기타\n"
|
||||
. "· 대행소 선택 시 판매 열만 해당 대행소 소속 판매소 기준\n"
|
||||
. "조회 후 표·엑셀·인쇄에 반영됩니다.";
|
||||
?>
|
||||
<div class="flow-print-sheet">
|
||||
<?= view('components/print_header', [
|
||||
@@ -27,9 +45,17 @@ if ($queried) {
|
||||
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<span class="text-sm font-bold text-gray-700">기간별 봉투 수불 현황</span>
|
||||
<span class="text-sm font-bold text-gray-700 inline-flex items-center gap-1">
|
||||
기간별 봉투 수불 현황
|
||||
<?= view('components/field_tooltip', ['text' => $tipPage, 'placement' => 'below']) ?>
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="<?= base_url('bag/flow/export?' . esc($exportQuery, 'attr')) ?>" class="bg-green-700 text-white px-3 py-1 rounded-sm text-sm hover:bg-green-800">엑셀저장</a>
|
||||
<?php if ($excelUrl !== ''): ?>
|
||||
<a href="<?= esc($excelUrl, 'attr') ?>" target="_blank" rel="noopener noreferrer"
|
||||
class="bg-green-700 text-white px-3 py-1 rounded-sm text-sm hover:bg-green-800">엑셀저장</a>
|
||||
<?php else: ?>
|
||||
<span class="bg-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm cursor-not-allowed" title="조회 후 이용">엑셀저장</span>
|
||||
<?php endif; ?>
|
||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">인쇄</button>
|
||||
<a href="<?= base_url('dashboard') ?>" class="border border-gray-400 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">종료</a>
|
||||
</div>
|
||||
@@ -103,7 +129,6 @@ if ($queried) {
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
||||
<a href="<?= base_url('bag/flow') ?>" class="text-gray-500 hover:text-gray-800 px-2">초기화</a>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-1">전일재고 = 조회 시작일 전날 기준 품목별 재고(입고·반품·기타 − 출고 누적). 대행소 선택 시 <strong>판매</strong>만 해당 대행소 소속 판매소 기준입니다.</p>
|
||||
</section>
|
||||
|
||||
<?php if (! $queried): ?>
|
||||
@@ -186,6 +211,26 @@ if ($queried) {
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field-tip { position: relative; display: inline-flex; vertical-align: middle; }
|
||||
.field-tip-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 14px; height: 14px; font-size: 10px; font-weight: 700; line-height: 1;
|
||||
color: #6b7280; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 50%;
|
||||
cursor: help; user-select: none;
|
||||
}
|
||||
.field-tip-btn:hover, .field-tip-btn:focus { color: #1d4ed8; border-color: #93c5fd; background: #eff6ff; outline: none; }
|
||||
.field-tip-panel {
|
||||
position: absolute; z-index: 60; left: 50%; transform: translateX(-50%);
|
||||
bottom: calc(100% + 6px); width: max-content; max-width: 300px;
|
||||
padding: 0.35rem 0.5rem; border-radius: 4px;
|
||||
background: #1f2937; color: #f9fafb; font-size: 11px; font-weight: 500; line-height: 1.35;
|
||||
text-align: left; white-space: pre-line; box-shadow: 0 2px 8px rgba(0,0,0,.15);
|
||||
opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .12s, visibility .12s;
|
||||
}
|
||||
.field-tip--below .field-tip-panel { bottom: auto; top: calc(100% + 6px); }
|
||||
.field-tip:hover .field-tip-panel,
|
||||
.field-tip:focus-within .field-tip-panel { opacity: 1; visibility: visible; }
|
||||
|
||||
.flow-lbl-print { display: none; }
|
||||
|
||||
@media screen {
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
<h2 class="text-lg font-bold text-gray-700 mb-4">도움말</h2>
|
||||
|
||||
<div class="space-y-4 text-sm text-gray-600">
|
||||
<section class="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<h3 class="font-bold text-blue-800 mb-1">📖 사용자 매뉴얼</h3>
|
||||
<p>업무별 사용 방법을 단계별로 정리한 사용자 설명서입니다.</p>
|
||||
<p class="mt-2"><a href="<?= base_url('bag/manual') ?>" class="text-blue-700 hover:underline font-semibold">사용자 매뉴얼 열기 →</a></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="font-bold text-gray-700 mb-1">시스템 개요</h3>
|
||||
<p>종량제 시스템은 지자체 종량제 쓰레기봉투의 발주, 입고, 재고, 판매, 불출 등 전체 물류 프로세스를 관리합니다.</p>
|
||||
@@ -23,6 +29,12 @@
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="font-bold text-gray-700 mb-1">번호알기</h3>
|
||||
<p>봉투 바코드 코드를 입력하면 <strong>바코드</strong>·<strong>인쇄숫자</strong>·<strong>인식번호</strong>를 확인할 수 있습니다.</p>
|
||||
<p class="mt-2"><a href="<?= base_url('bag/number-lookup') ?>" class="text-blue-700 hover:underline font-semibold">봉투번호확인(번호알기) 열기</a></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="font-bold text-gray-700 mb-1">문의</h3>
|
||||
<p>시스템 사용 중 문의사항은 소속 지자체 담당자에게 연락하시기 바랍니다.</p>
|
||||
|
||||
@@ -173,6 +173,15 @@ $lowStock = [
|
||||
<p class="text-[11px] text-gray-500 truncate">업무별 사용 방법 안내</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<?= base_url('dashboard/gov-portal') ?>" class="group flex items-center gap-3 px-3 py-2 rounded border border-gray-200 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
||||
<div class="h-8 w-8 rounded-full bg-slate-800 text-white flex items-center justify-center">
|
||||
<i class="fa-solid fa-building-columns"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold text-gray-900 group-hover:text-blue-800">종량제 시스템 · 포털</p>
|
||||
<p class="text-[11px] text-gray-500 truncate">기본 · 변형(strip) 시안</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<?= base_url('dashboard') ?>" class="group flex items-center gap-3 px-3 py-2 rounded border border-dashed border-gray-300 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
||||
<div class="h-8 w-8 rounded-full bg-white text-gray-500 flex items-center justify-center">
|
||||
<i class="fa-solid fa-table-columns"></i>
|
||||
|
||||
92
app/Views/bag/manual.php
Normal file
92
app/Views/bag/manual.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* 사용자 매뉴얼 뷰.
|
||||
*
|
||||
* @var array<string, array{title:string,file:string}> $pages 목차
|
||||
* @var string $current 현재 slug
|
||||
* @var string $title 현재 페이지 제목
|
||||
* @var string $body commonmark 로 변환된 HTML (신뢰된 콘텐츠)
|
||||
*/
|
||||
$pages = $pages ?? [];
|
||||
$current = (string) ($current ?? '');
|
||||
$title = (string) ($title ?? '사용자 매뉴얼');
|
||||
$body = (string) ($body ?? '');
|
||||
|
||||
$slugs = array_keys($pages);
|
||||
$pos = array_search($current, $slugs, true);
|
||||
$prevSlug = ($pos !== false && $pos > 0) ? $slugs[$pos - 1] : null;
|
||||
$nextSlug = ($pos !== false && $pos < count($slugs) - 1) ? $slugs[$pos + 1] : null;
|
||||
?>
|
||||
<style>
|
||||
/* 매뉴얼 본문 스코프 스타일 (사이트 레이아웃 Tailwind 에 typography 플러그인 없음) */
|
||||
.manual-prose { color: #1f2937; font-size: 0.95rem; line-height: 1.75; max-width: 52rem; }
|
||||
.manual-prose h1 { font-size: 1.6rem; font-weight: 800; margin: 0 0 1rem; color: #111827; }
|
||||
.manual-prose h2 { font-size: 1.25rem; font-weight: 700; margin: 1.8rem 0 0.7rem; padding-bottom: 0.35rem; border-bottom: 2px solid #e5e7eb; color: #1d4ed8; scroll-margin-top: 1rem; }
|
||||
.manual-prose h3 { font-size: 1.05rem; font-weight: 700; margin: 1.3rem 0 0.5rem; color: #374151; }
|
||||
.manual-prose p { margin: 0.6rem 0; }
|
||||
.manual-prose ul, .manual-prose ol { margin: 0.6rem 0 0.6rem 1.4rem; }
|
||||
.manual-prose ul { list-style: disc; }
|
||||
.manual-prose ol { list-style: decimal; }
|
||||
.manual-prose li { margin: 0.25rem 0; }
|
||||
.manual-prose li > ul, .manual-prose li > ol { margin-top: 0.25rem; }
|
||||
.manual-prose a { color: #1c4e80; text-decoration: underline; }
|
||||
.manual-prose a:hover { color: #2563eb; }
|
||||
.manual-prose strong { font-weight: 700; color: #111827; }
|
||||
.manual-prose blockquote { margin: 0.9rem 0; padding: 0.6rem 1rem; border-left: 4px solid #60a5fa; background: #eff6ff; color: #1e3a8a; border-radius: 0 6px 6px 0; }
|
||||
.manual-prose code { background: #f3f4f6; color: #b91c1c; padding: 0.1rem 0.35rem; border-radius: 4px; font-size: 0.85em; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.manual-prose pre { background: #1e293b; color: #e2e8f0; padding: 0.9rem 1rem; border-radius: 8px; overflow-x: auto; margin: 0.9rem 0; font-size: 0.85rem; line-height: 1.6; }
|
||||
.manual-prose pre code { background: transparent; color: inherit; padding: 0; }
|
||||
.manual-prose table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: 0.875rem; }
|
||||
.manual-prose th, .manual-prose td { border: 1px solid #d1d5db; padding: 0.45rem 0.7rem; text-align: left; vertical-align: top; }
|
||||
.manual-prose th { background: #e9ecef; font-weight: 700; color: #333; }
|
||||
.manual-prose tbody tr:nth-child(even) td { background: #f9fafb; }
|
||||
.manual-prose hr { margin: 1.6rem 0; border: 0; border-top: 1px solid #e5e7eb; }
|
||||
.manual-toc a.active { background: #1c4e80; color: #fff; font-weight: 700; }
|
||||
@media print {
|
||||
.manual-toc, .manual-actions, .manual-nav { display: none !important; }
|
||||
.manual-layout { display: block !important; }
|
||||
.manual-prose { max-width: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="manual-layout flex gap-6 items-start max-w-6xl mx-auto">
|
||||
<!-- 좌측 목차 -->
|
||||
<nav class="manual-toc no-print w-56 shrink-0 sticky top-0 self-start">
|
||||
<div class="bg-title-bar text-white text-sm font-bold px-3 py-2 rounded-t">사용자 매뉴얼</div>
|
||||
<ul class="border border-t-0 border-gray-300 rounded-b divide-y divide-gray-100 bg-white text-sm">
|
||||
<?php foreach ($pages as $slug => $p): ?>
|
||||
<li>
|
||||
<a href="<?= base_url('bag/manual/' . $slug) ?>"
|
||||
class="block px-3 py-2 text-gray-700 hover:bg-blue-50 <?= $slug === $current ? 'active' : '' ?>">
|
||||
<?= esc($p['title']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 본문 -->
|
||||
<div class="flex-grow min-w-0">
|
||||
<div class="manual-actions no-print flex justify-end mb-3">
|
||||
<button type="button" onclick="window.print()"
|
||||
class="text-sm border border-gray-300 rounded px-3 py-1.5 text-gray-600 hover:bg-gray-50">
|
||||
인쇄
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<article class="manual-prose"><?= $body ?></article>
|
||||
|
||||
<!-- 이전/다음 -->
|
||||
<div class="manual-nav no-print flex justify-between mt-8 pt-4 border-t border-gray-200 text-sm">
|
||||
<?php if ($prevSlug !== null): ?>
|
||||
<a href="<?= base_url('bag/manual/' . $prevSlug) ?>" class="text-blue-700 hover:underline">← <?= esc($pages[$prevSlug]['title']) ?></a>
|
||||
<?php else: ?>
|
||||
<span></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($nextSlug !== null): ?>
|
||||
<a href="<?= base_url('bag/manual/' . $nextSlug) ?>" class="text-blue-700 hover:underline"><?= esc($pages[$nextSlug]['title']) ?> →</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
307
app/Views/bag/number_lookup.php
Normal file
307
app/Views/bag/number_lookup.php
Normal file
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$code = (string) ($code ?? '');
|
||||
$result = is_array($result ?? null) ? $result : null;
|
||||
$barcodeText = (string) ($result['barcode_text'] ?? '- - - -');
|
||||
$printText = (string) ($result['print_text'] ?? '- - -');
|
||||
$recognition = (string) ($result['recognition_text'] ?? '- -');
|
||||
$error = (string) ($error ?? ($result['ok'] ?? true ? '' : ($result['message'] ?? '')));
|
||||
$unit = (string) ($result['unit'] ?? '');
|
||||
$bagName = (string) ($result['bag_name'] ?? '');
|
||||
$bagCode = (string) ($result['bag_code'] ?? '');
|
||||
$hasResult = $result !== null && ($result['ok'] ?? false);
|
||||
?>
|
||||
<style>
|
||||
.num-lookup-wrap {
|
||||
max-width: 28rem;
|
||||
margin: 1.5rem auto 2rem;
|
||||
border: 1px solid #9ca3af;
|
||||
background: #ece9d8;
|
||||
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.08);
|
||||
font-family: 'Malgun Gothic', 'Noto Sans KR', sans-serif;
|
||||
}
|
||||
.num-lookup-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.55rem;
|
||||
background: linear-gradient(180deg, #3a6ea5 0%, #2c5282 100%);
|
||||
color: #fff;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.num-lookup-body { padding: 0.85rem 0.9rem 1rem; }
|
||||
.num-lookup-label {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.num-lookup-input {
|
||||
width: 100%;
|
||||
border: 1px solid #9ca3af;
|
||||
background: #fff;
|
||||
padding: 0.35rem 0.45rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
.num-lookup-input:focus {
|
||||
outline: 1px solid #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
.num-lookup-sep {
|
||||
border: none;
|
||||
border-top: 1px solid #b8b4a8;
|
||||
margin: 0.85rem 0;
|
||||
}
|
||||
.num-lookup-row {
|
||||
display: grid;
|
||||
grid-template-columns: 5.5rem 1fr;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.55rem;
|
||||
}
|
||||
.num-lookup-row label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
text-align: right;
|
||||
}
|
||||
.num-lookup-out {
|
||||
min-height: 1.75rem;
|
||||
border: 1px solid #9ca3af;
|
||||
background: #fffef0;
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
letter-spacing: 0.04em;
|
||||
color: #111827;
|
||||
}
|
||||
.num-lookup-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.num-lookup-btn {
|
||||
border: 1px solid #6b7280;
|
||||
background: linear-gradient(180deg, #f9fafb 0%, #e5e7eb 100%);
|
||||
padding: 0.3rem 0.85rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.num-lookup-btn-primary {
|
||||
border-color: #1c4e80;
|
||||
background: linear-gradient(180deg, #2b6cb0 0%, #1c4e80 100%);
|
||||
color: #fff;
|
||||
}
|
||||
.num-lookup-meta {
|
||||
margin-top: 0.65rem;
|
||||
font-size: 0.75rem;
|
||||
color: #4b5563;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.num-lookup-error {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.num-lookup-hint {
|
||||
margin: 0.75rem auto 0;
|
||||
max-width: 28rem;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="num-lookup-wrap" role="dialog" aria-labelledby="numLookupTitle">
|
||||
<div class="num-lookup-title" id="numLookupTitle">
|
||||
<i class="fa-solid fa-trash-can" aria-hidden="true"></i>
|
||||
봉투번호확인
|
||||
</div>
|
||||
<form class="num-lookup-body" method="get" action="<?= site_url('bag/number-lookup') ?>" id="numLookupForm">
|
||||
<label class="num-lookup-label" for="codeInput">코드 입력</label>
|
||||
<input
|
||||
type="text"
|
||||
id="codeInput"
|
||||
name="code"
|
||||
class="num-lookup-input"
|
||||
value="<?= esc($code) ?>"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder="예: OQXCKH-000008-P299-S00125"
|
||||
/>
|
||||
|
||||
<hr class="num-lookup-sep"/>
|
||||
|
||||
<div class="num-lookup-row">
|
||||
<label for="barcodeOut">바코드</label>
|
||||
<div id="barcodeOut" class="num-lookup-out" aria-live="polite"><?= esc($barcodeText) ?></div>
|
||||
</div>
|
||||
<div class="num-lookup-row">
|
||||
<label for="printOut">인쇄숫자</label>
|
||||
<div id="printOut" class="num-lookup-out" aria-live="polite"><?= esc($printText) ?></div>
|
||||
</div>
|
||||
<div class="num-lookup-row">
|
||||
<label for="recognitionOut">인식번호</label>
|
||||
<div id="recognitionOut" class="num-lookup-out" aria-live="polite"><?= esc($recognition) ?></div>
|
||||
</div>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<p class="num-lookup-error" id="numLookupError"><?= esc($error) ?></p>
|
||||
<?php else: ?>
|
||||
<p class="num-lookup-error" id="numLookupError" hidden></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($hasResult && ($unit !== '' || $bagName !== '')): ?>
|
||||
<p class="num-lookup-meta" id="numLookupMeta">
|
||||
<?php if ($unit !== ''): ?>단위: <?= esc($unit) ?><?php endif; ?>
|
||||
<?php if ($bagName !== ''): ?> · <?= esc($bagName) ?><?= $bagCode !== '' ? ' (' . esc($bagCode) . ')' : '' ?><?php endif; ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p class="num-lookup-meta" id="numLookupMeta" hidden></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="num-lookup-actions">
|
||||
<button type="button" class="num-lookup-btn" id="numLookupReset">초기화</button>
|
||||
<button type="submit" class="num-lookup-btn num-lookup-btn-primary">확인</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="num-lookup-hint">
|
||||
봉투 바코드·LOT·팩·낱장 코드를 입력하면 <strong>바코드</strong>(4칸), <strong>인쇄숫자</strong>(3칸), <strong>인식번호</strong>(2칸)로 나누어 표시합니다.
|
||||
등록된 입고 바코드는 DB에서 품목명을 함께 조회합니다.
|
||||
</p>
|
||||
|
||||
<div class="mx-auto mt-4 max-w-xl text-xs text-gray-700">
|
||||
<table class="w-full border border-gray-300 border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="border border-gray-300 px-2 py-1 text-left">단위</th>
|
||||
<th class="border border-gray-300 px-2 py-1 text-left">입력 예시</th>
|
||||
<th class="border border-gray-300 px-2 py-1 text-left">설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top">LOT</td>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top"><code>OQXCKH</code></td>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top">
|
||||
발주 LOT 번호만 입력합니다.<br>
|
||||
바코드: <code>OQXCKH - - -</code><br>
|
||||
인쇄숫자/인식번호는 LOT 기준으로만 표시됩니다.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top">박스</td>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top"><code>OQXCKH-000008-B001</code></td>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top">
|
||||
박스 바코드(LOT-입고번호-B박스번호)를 그대로 입력합니다.<br>
|
||||
바코드: <code>LOT 입고번호 B박스 -</code><br>
|
||||
인쇄숫자: <code>입고번호 박스번호 -</code><br>
|
||||
인식번호: <code>입고번호 B박스</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top">팩</td>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top"><code>OQXCKH-000008-P299</code></td>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top">
|
||||
팩 바코드(LOT-입고번호-P팩번호)를 그대로 입력합니다.<br>
|
||||
바코드: <code>LOT 입고번호 P팩 -</code><br>
|
||||
인쇄숫자: <code>입고번호 팩번호 -</code><br>
|
||||
인식번호: <code>입고번호 P팩</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top">낱장</td>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top"><code>OQXCKH-000008-P299-S00125</code></td>
|
||||
<td class="border border-gray-300 px-2 py-1 align-top">
|
||||
낱장 바코드(LOT-입고번호-P팩-S장번호)를 그대로 입력합니다.<br>
|
||||
바코드: <code>LOT 입고번호 P팩 S장번호</code><br>
|
||||
인쇄숫자: <code>입고번호 팩번호 장번호</code><br>
|
||||
인식번호: <code>입고번호 P팩</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var form = document.getElementById('numLookupForm');
|
||||
var input = document.getElementById('codeInput');
|
||||
var resetBtn = document.getElementById('numLookupReset');
|
||||
if (!form || !input) return;
|
||||
|
||||
var emptyBarcode = '- - - -';
|
||||
var emptyPrint = '- - -';
|
||||
var emptyRec = '- -';
|
||||
|
||||
function setOutputs(data) {
|
||||
document.getElementById('barcodeOut').textContent = data.barcode_text || emptyBarcode;
|
||||
document.getElementById('printOut').textContent = data.print_text || emptyPrint;
|
||||
document.getElementById('recognitionOut').textContent = data.recognition_text || emptyRec;
|
||||
var err = document.getElementById('numLookupError');
|
||||
var meta = document.getElementById('numLookupMeta');
|
||||
if (err) {
|
||||
if (data.message) {
|
||||
err.textContent = data.message;
|
||||
err.hidden = false;
|
||||
} else {
|
||||
err.textContent = '';
|
||||
err.hidden = true;
|
||||
}
|
||||
}
|
||||
if (meta) {
|
||||
var parts = [];
|
||||
if (data.unit) parts.push('단위: ' + data.unit);
|
||||
if (data.bag_name) {
|
||||
var line = data.bag_name;
|
||||
if (data.bag_code) line += ' (' + data.bag_code + ')';
|
||||
parts.push(line);
|
||||
}
|
||||
if (parts.length) {
|
||||
meta.textContent = parts.join(' · ');
|
||||
meta.hidden = false;
|
||||
} else {
|
||||
meta.hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lookupAjax() {
|
||||
var code = (input.value || '').trim();
|
||||
if (!code) {
|
||||
setOutputs({ message: '코드를 입력해 주세요.', barcode_text: emptyBarcode, print_text: emptyPrint, recognition_text: emptyRec });
|
||||
return;
|
||||
}
|
||||
fetch('<?= site_url('bag/number-lookup/resolve') ?>', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: 'code=' + encodeURIComponent(code) + '&<?= csrf_token() ?>=' + encodeURIComponent('<?= csrf_hash() ?>')
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) { setOutputs(data || {}); })
|
||||
.catch(function () { setOutputs({ message: '조회 중 오류가 발생했습니다.' }); });
|
||||
}
|
||||
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
lookupAjax();
|
||||
}
|
||||
});
|
||||
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function () {
|
||||
input.value = '';
|
||||
setOutputs({ barcode_text: emptyBarcode, print_text: emptyPrint, recognition_text: emptyRec });
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
10
app/Views/components/field_tooltip.php
Normal file
10
app/Views/components/field_tooltip.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** @var string $text */
|
||||
$placement = (string) ($placement ?? 'above');
|
||||
$wrapClass = 'field-tip' . ($placement === 'below' ? ' field-tip--below' : '');
|
||||
?>
|
||||
<span class="<?= esc($wrapClass, 'attr') ?>">
|
||||
<span class="field-tip-btn" tabindex="0" aria-label="설명">ⓘ</span>
|
||||
<span class="field-tip-panel" role="tooltip"><?= esc($text) ?></span>
|
||||
</span>
|
||||
15
app/Views/home/_dashboard_gov_portal_brand.php
Normal file
15
app/Views/home/_dashboard_gov_portal_brand.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* 포털 헤더 브랜드 — components/header_brand 와 동일 SVG·문구
|
||||
*
|
||||
* @var string $brandHref
|
||||
*/
|
||||
$brandHref = $brandHref ?? base_url('dashboard/gov-portal');
|
||||
?>
|
||||
<a href="<?= esc($brandHref) ?>" class="gov-portal-brand">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
|
||||
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/>
|
||||
</svg>
|
||||
<span>종량제 시스템</span>
|
||||
</a>
|
||||
13
app/Views/home/_dashboard_gov_portal_brand_css.php
Normal file
13
app/Views/home/_dashboard_gov_portal_brand_css.php
Normal file
@@ -0,0 +1,13 @@
|
||||
.gov-portal-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gov-portal-brand:hover { color: #fff; opacity: 0.92; }
|
||||
.gov-portal-brand svg { width: 24px; height: 24px; flex-shrink: 0; opacity: 0.95; }
|
||||
15
app/Views/home/_dashboard_gov_portal_font_zoom_script.php
Normal file
15
app/Views/home/_dashboard_gov_portal_font_zoom_script.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<script>
|
||||
(function () {
|
||||
var scale = 1;
|
||||
var label = document.getElementById('fontZoomLabel');
|
||||
var root = document.documentElement;
|
||||
function applyScale() {
|
||||
root.style.setProperty('--font-scale', String(scale));
|
||||
if (label) label.textContent = Math.round(scale * 100) + '%';
|
||||
}
|
||||
var btnSm = document.getElementById('fontSmaller');
|
||||
var btnLg = document.getElementById('fontLarger');
|
||||
if (btnSm) btnSm.addEventListener('click', function () { scale = Math.max(0.85, scale - 0.05); applyScale(); });
|
||||
if (btnLg) btnLg.addEventListener('click', function () { scale = Math.min(1.2, scale + 0.05); applyScale(); });
|
||||
})();
|
||||
</script>
|
||||
3
app/Views/home/_dashboard_gov_portal_head.php
Normal file
3
app/Views/home/_dashboard_gov_portal_head.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>종량제 시스템</title>
|
||||
24
app/Views/home/_dashboard_gov_portal_header_utils.php
Normal file
24
app/Views/home/_dashboard_gov_portal_header_utils.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** @var string $activeVariant */
|
||||
/** @var array $portalVariants */
|
||||
/** @var string $mbName */
|
||||
/** @var string $levelName */
|
||||
/** @var string $lgLabel */
|
||||
/** @var bool $showFontZoom */
|
||||
$showFontZoom = $showFontZoom ?? true;
|
||||
?>
|
||||
<div class="portal-header-utils">
|
||||
<?= view('home/_dashboard_gov_portal_variant_nav', ['portalVariants' => $portalVariants, 'activeVariant' => $activeVariant]) ?>
|
||||
<span class="user-line"><?= esc($mbName) ?> · <?= esc($levelName) ?> · <?= esc($lgLabel) ?></span>
|
||||
<?php if ($showFontZoom): ?>
|
||||
<button type="button" class="extend-btn" onclick="alert('세션 연장(목업)')">시간연장</button>
|
||||
<div class="font-zoom" title="글자 크기">
|
||||
<button type="button" id="fontSmaller" aria-label="글자 작게">−</button>
|
||||
<span id="fontZoomLabel">100%</span>
|
||||
<button type="button" id="fontLarger" aria-label="글자 크게">+</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<a href="<?= base_url('dashboard/simple') ?>" class="util-ico" title="기존 요약"><i class="fa-solid fa-table-columns"></i></a>
|
||||
<a href="<?= base_url('logout') ?>" class="util-ico" title="로그아웃"><i class="fa-solid fa-right-from-bracket"></i></a>
|
||||
</div>
|
||||
71
app/Views/home/_dashboard_gov_portal_map_css.php
Normal file
71
app/Views/home/_dashboard_gov_portal_map_css.php
Normal file
@@ -0,0 +1,71 @@
|
||||
.portal-map-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
background: #e8eef4;
|
||||
}
|
||||
.portal-map-wrap.is-fill { height: 100%; min-height: 220px; }
|
||||
.portal-map-leaflet {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: inherit;
|
||||
z-index: 1;
|
||||
}
|
||||
.portal-map-wrap .leaflet-control-attribution {
|
||||
font-size: 0.5625rem;
|
||||
line-height: 1.2;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
.portal-map-legend {
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
z-index: 500;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
padding: 0.3rem 0.45rem;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 6px;
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
.portal-map-legend span::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.2rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.portal-map-legend .lg-warehouse::before { background: #2563eb; }
|
||||
.portal-map-legend .lg-shop::before { background: #10b981; }
|
||||
.portal-map-legend .lg-flow::before { background: #f59e0b; }
|
||||
.portal-map-lg-badge {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
z-index: 500;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 0.35rem 0.55rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
color: #1a2b4b;
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
|
||||
pointer-events: none;
|
||||
max-width: calc(100% - 1rem);
|
||||
}
|
||||
.portal-map-lg-badge small {
|
||||
display: block;
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" crossorigin="anonymous" referrerpolicy="no-referrer"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
117
app/Views/home/_dashboard_gov_portal_map_panel.php
Normal file
117
app/Views/home/_dashboard_gov_portal_map_panel.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* OpenStreetMap(Leaflet) 목업 지도 — 지정판매소·창고·수불 지점 대략 표시
|
||||
*
|
||||
* @var string $lgLabel
|
||||
* @var string $mapId
|
||||
* @var string $mapHeight
|
||||
* @var bool $mapFill
|
||||
* @var array{centerLat:float,centerLng:float,zoom:int,markers:list<array>} $govMapPanel
|
||||
*/
|
||||
$mapId = $mapId ?? 'govPortalMap';
|
||||
$mapHeight = $mapHeight ?? '200px';
|
||||
$mapFill = $mapFill ?? false;
|
||||
$govMapPanel = $govMapPanel ?? [
|
||||
'centerLat' => 35.8714,
|
||||
'centerLng' => 128.6014,
|
||||
'zoom' => 11,
|
||||
'markers' => [],
|
||||
];
|
||||
$wrapClass = 'portal-map-wrap' . ($mapFill ? ' is-fill' : '');
|
||||
$wrapStyle = $mapFill ? '' : 'height:' . esc($mapHeight, 'attr') . ';';
|
||||
?>
|
||||
<div class="<?= esc($wrapClass, 'attr') ?>" id="<?= esc($mapId, 'attr') ?>-wrap"<?= $wrapStyle !== '' ? ' style="' . $wrapStyle . '"' : '' ?>>
|
||||
<div class="portal-map-lg-badge">
|
||||
<?= esc($lgLabel) ?>
|
||||
<small>지정판매소·창고 (목업)</small>
|
||||
</div>
|
||||
<div id="<?= esc($mapId, 'attr') ?>" class="portal-map-leaflet" role="application" aria-label="<?= esc($lgLabel) ?> 판매·수불 지도"></div>
|
||||
<div class="portal-map-legend" aria-hidden="true">
|
||||
<span class="lg-warehouse">창고</span>
|
||||
<span class="lg-shop">판매소</span>
|
||||
<span class="lg-flow">수불</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var mapId = <?= json_encode($mapId, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||
var cfg = <?= json_encode($govMapPanel, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>;
|
||||
var lgLabel = <?= json_encode($lgLabel, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>;
|
||||
|
||||
function markerStyle(kind) {
|
||||
if (kind === 'warehouse') return { color: '#2563eb', fill: '#3b82f6' };
|
||||
if (kind === 'shop') return { color: '#059669', fill: '#10b981' };
|
||||
return { color: '#d97706', fill: '#f59e0b' };
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
var el = document.getElementById(mapId);
|
||||
if (!el || typeof L === 'undefined') return;
|
||||
|
||||
if (el._govPortalMap) {
|
||||
el._govPortalMap.remove();
|
||||
el._govPortalMap = null;
|
||||
}
|
||||
|
||||
var map = L.map(el, {
|
||||
zoomControl: true,
|
||||
scrollWheelZoom: false,
|
||||
attributionControl: true,
|
||||
}).setView([cfg.centerLat, cfg.centerLng], cfg.zoom || 11);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 18,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a>',
|
||||
}).addTo(map);
|
||||
|
||||
(cfg.markers || []).forEach(function (m) {
|
||||
var st = markerStyle(m.kind || 'flow');
|
||||
var circle = L.circleMarker([m.lat, m.lng], {
|
||||
radius: 7,
|
||||
color: st.color,
|
||||
fillColor: st.fill,
|
||||
fillOpacity: 0.85,
|
||||
weight: 2,
|
||||
}).addTo(map);
|
||||
if (m.title) {
|
||||
circle.bindPopup('<strong>' + m.title + '</strong>');
|
||||
}
|
||||
});
|
||||
|
||||
L.circleMarker([cfg.centerLat, cfg.centerLng], {
|
||||
radius: 9,
|
||||
color: '#1a2b4b',
|
||||
fillColor: '#4a69bd',
|
||||
fillOpacity: 0.9,
|
||||
weight: 2,
|
||||
}).addTo(map).bindPopup('<strong>' + lgLabel + '</strong><br>담당 지자체');
|
||||
|
||||
var bounds = [];
|
||||
(cfg.markers || []).forEach(function (m) { bounds.push([m.lat, m.lng]); });
|
||||
if (bounds.length > 1) {
|
||||
map.fitBounds(bounds, { padding: [28, 28], maxZoom: 13 });
|
||||
}
|
||||
|
||||
el._govPortalMap = map;
|
||||
setTimeout(function () { map.invalidateSize(); }, 120);
|
||||
}
|
||||
|
||||
function boot() {
|
||||
if (typeof L !== 'undefined') {
|
||||
initMap();
|
||||
return;
|
||||
}
|
||||
var el = document.getElementById(mapId);
|
||||
if (el) {
|
||||
el.innerHTML = '<div style="padding:1rem;text-align:center;font-size:12px;color:#666;">지도를 불러오지 못했습니다. 네트워크 연결을 확인해 주세요.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', boot);
|
||||
} else {
|
||||
boot();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
43
app/Views/home/_dashboard_gov_portal_menu_search.php
Normal file
43
app/Views/home/_dashboard_gov_portal_menu_search.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* 메뉴검색 + 하단 바로가기 칩
|
||||
*
|
||||
* @var string $variant teal|inline
|
||||
* @var string $inputId
|
||||
* @var list<array{label:string,url:string,keyword:string}> $menuSearchOptions
|
||||
*/
|
||||
$variant = $variant ?? 'teal';
|
||||
$inputId = $inputId ?? 'menuSearch';
|
||||
$menuSearchOptions = $menuSearchOptions ?? [];
|
||||
?>
|
||||
<?php if ($variant === 'inline'): ?>
|
||||
<div class="search-inline portal-menu-search-block">
|
||||
<div class="search-inline-row">
|
||||
<label for="<?= esc($inputId, 'attr') ?>"><i class="fa-solid fa-magnifying-glass"></i> 메뉴검색</label>
|
||||
<input type="search" id="<?= esc($inputId, 'attr') ?>" placeholder="메뉴명 (예: 재고, 발주)" autocomplete="off"/>
|
||||
</div>
|
||||
<?php if ($menuSearchOptions !== []): ?>
|
||||
<div class="portal-menu-search-options" aria-label="자주 찾는 메뉴">
|
||||
<?php foreach ($menuSearchOptions as $opt): ?>
|
||||
<a href="#" class="menu-search-chip" data-url="<?= esc($opt['url'], 'attr') ?>" data-keyword="<?= esc($opt['keyword'], 'attr') ?>" title="<?= esc($opt['label']) ?>"><?= esc($opt['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="search-teal portal-menu-search-block">
|
||||
<strong><i class="fa-solid fa-magnifying-glass"></i> 메뉴검색</strong>
|
||||
<div class="search-wrap">
|
||||
<input type="search" id="<?= esc($inputId, 'attr') ?>" placeholder="메뉴명 입력 (예: 재고, 발주, 통계)" autocomplete="off"/>
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
</div>
|
||||
<?php if ($menuSearchOptions !== []): ?>
|
||||
<div class="portal-menu-search-options" aria-label="자주 찾는 메뉴">
|
||||
<?php foreach ($menuSearchOptions as $opt): ?>
|
||||
<a href="#" class="menu-search-chip" data-url="<?= esc($opt['url'], 'attr') ?>" data-keyword="<?= esc($opt['keyword'], 'attr') ?>" title="<?= esc($opt['label']) ?>"><?= esc($opt['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
48
app/Views/home/_dashboard_gov_portal_menu_search_css.php
Normal file
48
app/Views/home/_dashboard_gov_portal_menu_search_css.php
Normal file
@@ -0,0 +1,48 @@
|
||||
.portal-menu-search-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.menu-search-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 0.22rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.02em;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.search-teal .menu-search-chip,
|
||||
.search-inline .menu-search-chip {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
color: #fff;
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.search-teal .menu-search-chip:hover,
|
||||
.search-inline .menu-search-chip:hover {
|
||||
background: #fff;
|
||||
color: #00796b;
|
||||
border-color: #fff;
|
||||
}
|
||||
.search-inline.portal-menu-search-block {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.search-inline.portal-menu-search-block .search-inline-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.search-inline.portal-menu-search-block input {
|
||||
margin-top: 0;
|
||||
}
|
||||
118
app/Views/home/_dashboard_gov_portal_nav_script_base.php
Normal file
118
app/Views/home/_dashboard_gov_portal_nav_script_base.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<script>
|
||||
(function () {
|
||||
var navData = <?= $govNavJson ?? '[]' ?>;
|
||||
var activeIdx = <?= (int) ($govActiveParentIdx ?? 0) ?>;
|
||||
var activeChildHref = <?= json_encode(strtolower(ltrim((string) ($govActiveChildHref ?? ''), '/')), JSON_UNESCAPED_UNICODE) ?>;
|
||||
var listEl = document.getElementById('portalSidebarList');
|
||||
var titleEl = document.getElementById('portalSidebarTitle');
|
||||
|
||||
if (listEl && navData.length) {
|
||||
function renderSidebar(idx) {
|
||||
var parent = navData[idx];
|
||||
if (!parent) return;
|
||||
if (titleEl) titleEl.textContent = parent.name || 'MY MENU';
|
||||
listEl.innerHTML = '';
|
||||
var items = parent.children && parent.children.length ? parent.children : null;
|
||||
if (!items && parent.href) {
|
||||
items = [{ name: parent.name, url: parent.url, href: parent.href }];
|
||||
}
|
||||
if (!items || !items.length) {
|
||||
var empty = document.createElement('li');
|
||||
empty.innerHTML = '<span class="menu-sub" style="opacity:.65;">하위 메뉴 없음</span>';
|
||||
listEl.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
items.forEach(function (child, ci) {
|
||||
var li = document.createElement('li');
|
||||
var chHref = (child.href || '').toLowerCase().replace(/^\//, '');
|
||||
var on = activeChildHref ? (chHref === activeChildHref) : (ci === 0);
|
||||
if (child.href) {
|
||||
li.innerHTML = '<a href="' + child.url + '" class="' + (on ? 'active' : '') + '">' +
|
||||
'<span class="menu-ico">' + (on ? '×' : '+') + '</span>' + child.name + '</a>';
|
||||
} else {
|
||||
li.innerHTML = '<span class="menu-sub" style="opacity:.65;"><span class="menu-ico">+</span>' + child.name + '</span>';
|
||||
}
|
||||
listEl.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveTrigger(idx) {
|
||||
document.querySelectorAll('.portal-nav-trigger, .portal-nav-link[data-parent-idx]').forEach(function (el) {
|
||||
var n = parseInt(el.getAttribute('data-parent-idx'), 10);
|
||||
var on = n === idx;
|
||||
el.classList.toggle('is-active', on);
|
||||
if (el.classList.contains('portal-nav-trigger')) {
|
||||
el.setAttribute('aria-expanded', on ? 'true' : 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.portal-nav-trigger[data-parent-idx]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
var idx = parseInt(btn.getAttribute('data-parent-idx'), 10);
|
||||
setActiveTrigger(idx);
|
||||
renderSidebar(idx);
|
||||
});
|
||||
});
|
||||
|
||||
setActiveTrigger(activeIdx);
|
||||
renderSidebar(activeIdx);
|
||||
}
|
||||
|
||||
var searchInput = document.getElementById('menuSearch') || document.getElementById('menuSearchStrip');
|
||||
|
||||
function runMenuSearch(q) {
|
||||
if (!q || !navData.length) return false;
|
||||
q = q.toLowerCase();
|
||||
for (var p = 0; p < navData.length; p++) {
|
||||
var par = navData[p];
|
||||
if ((par.name || '').toLowerCase().indexOf(q) !== -1) {
|
||||
if (listEl) {
|
||||
document.querySelectorAll('.portal-nav-trigger[data-parent-idx]').forEach(function (btn) {
|
||||
if (parseInt(btn.getAttribute('data-parent-idx'), 10) === p) btn.click();
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var ch = par.children || [];
|
||||
for (var i = 0; i < ch.length; i++) {
|
||||
if ((ch[i].name || '').toLowerCase().indexOf(q) !== -1 && ch[i].url) {
|
||||
window.location.href = ch[i].url;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (searchInput && navData.length) {
|
||||
searchInput.addEventListener('keydown', function (e) {
|
||||
if (e.key !== 'Enter') return;
|
||||
var q = (searchInput.value || '').trim();
|
||||
if (!q) return;
|
||||
if (!runMenuSearch(q)) {
|
||||
alert('일치하는 메뉴가 없습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.menu-search-chip').forEach(function (chip) {
|
||||
chip.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
var url = chip.getAttribute('data-url') || '';
|
||||
var kw = chip.getAttribute('data-keyword') || '';
|
||||
if (url) {
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
if (searchInput && kw) {
|
||||
searchInput.value = kw;
|
||||
if (!runMenuSearch(kw)) {
|
||||
alert('일치하는 메뉴가 없습니다.');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
10
app/Views/home/_dashboard_gov_portal_shared.php
Normal file
10
app/Views/home/_dashboard_gov_portal_shared.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @deprecated 데이터는 Home 컨트롤러의 gov_portal_dashboard_view_data()로 전달합니다.
|
||||
* 하위 호환용: include 시 컨트롤러와 동일한 변수를 주입합니다.
|
||||
*/
|
||||
helper('admin');
|
||||
foreach (gov_portal_dashboard_view_data($lgLabel ?? '북구', $activeVariant ?? 'base') as $k => $v) {
|
||||
$$k = $v;
|
||||
}
|
||||
59
app/Views/home/_dashboard_gov_portal_sidebar.php
Normal file
59
app/Views/home/_dashboard_gov_portal_sidebar.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** @var list<array> $govNavItems */
|
||||
/** @var int $govActiveParentIdx */
|
||||
/** @var string $govActiveChildHref */
|
||||
$activeParent = $govNavItems[$govActiveParentIdx] ?? $govNavItems[0] ?? null;
|
||||
$sidebarTitle = $activeParent['name'] ?? 'MY MENU';
|
||||
$activeChildHref = strtolower(ltrim((string) ($govActiveChildHref ?? ''), '/'));
|
||||
?>
|
||||
<aside class="sidebar">
|
||||
<div class="my-menu-hd" id="portalSidebarTitle"><?= esc($sidebarTitle) ?></div>
|
||||
<ul class="my-menu-list" id="portalSidebarList">
|
||||
<?php if ($activeParent !== null): ?>
|
||||
<?php if (! empty($activeParent['hasChildren'])): ?>
|
||||
<?php foreach ($activeParent['children'] as $ci => $child): ?>
|
||||
<?php
|
||||
$childHref = strtolower(ltrim((string) ($child['href'] ?? ''), '/'));
|
||||
$isChildActive = $activeChildHref !== ''
|
||||
? ($childHref === $activeChildHref)
|
||||
: ($ci === 0);
|
||||
?>
|
||||
<li>
|
||||
<?php if ($child['href'] !== ''): ?>
|
||||
<a href="<?= esc($child['url']) ?>" class="<?= $isChildActive ? 'active' : '' ?>">
|
||||
<span class="menu-ico"><?= $isChildActive ? '×' : '+' ?></span>
|
||||
<?= esc($child['name']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="menu-sub" style="opacity:.65;cursor:default;">
|
||||
<span class="menu-ico">+</span><?= esc($child['name']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php elseif ($activeParent['href'] !== ''): ?>
|
||||
<li>
|
||||
<a href="<?= esc($activeParent['url']) ?>" class="active">
|
||||
<span class="menu-ico">×</span><?= esc($activeParent['name']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<div class="sidebar-blocks">
|
||||
<div class="sb-teal">
|
||||
<i class="fa-solid fa-mobile-screen"></i>
|
||||
모바일 앱(예정)<br/>지정판매소 판매·스캔 연동
|
||||
</div>
|
||||
<div class="sb-gray">
|
||||
<i class="fa-solid fa-repeat"></i> 통합 전환<br/>
|
||||
<a href="<?= base_url('admin/select-local-government') ?>" style="color:#fff;text-decoration:underline;">지자체 선택</a>
|
||||
</div>
|
||||
<div class="sb-links">
|
||||
<a href="<?= base_url('bag/help') ?>">나의 할일</a>
|
||||
<a href="<?= base_url('dashboard') ?>">종합·그래프</a>
|
||||
<a href="<?= base_url('bag/help') ?>">FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
23
app/Views/home/_dashboard_gov_portal_stock_alert_levels.php
Normal file
23
app/Views/home/_dashboard_gov_portal_stock_alert_levels.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* 재고 경보 4단계 + 단계별 봉투 종류(대략)
|
||||
*
|
||||
* @var list<array{count:int,label:string,class:string,bags:list<string>}> $stockAlerts
|
||||
*/
|
||||
?>
|
||||
<div class="alert-levels">
|
||||
<?php foreach ($stockAlerts as $alert): ?>
|
||||
<div class="alert-box <?= esc($alert['class'], 'attr') ?>">
|
||||
<div class="n"><?= (int) $alert['count'] ?></div>
|
||||
<div class="t"><?= esc($alert['label']) ?></div>
|
||||
<?php if (! empty($alert['bags'])): ?>
|
||||
<ul class="alert-bags" aria-label="<?= esc($alert['label']) ?> 봉투 종류">
|
||||
<?php foreach ($alert['bags'] as $bagName): ?>
|
||||
<li><?= esc($bagName) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
157
app/Views/home/_dashboard_gov_portal_stock_cards_css.php
Normal file
157
app/Views/home/_dashboard_gov_portal_stock_cards_css.php
Normal file
@@ -0,0 +1,157 @@
|
||||
.alert-levels {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.alert-box {
|
||||
text-align: center;
|
||||
padding: 0.5rem 0.2rem 0.4375rem;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
.alert-box .n { font-size: 1.25rem; line-height: 1.1; letter-spacing: -0.03em; }
|
||||
.alert-box .t {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
margin-top: 0.125rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.alert-box .alert-bags {
|
||||
list-style: none;
|
||||
margin: 0.35rem 0 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.02em;
|
||||
opacity: 0.92;
|
||||
text-align: center;
|
||||
}
|
||||
.alert-box .alert-bags li {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0.05rem 0;
|
||||
}
|
||||
.al-yellow .alert-bags { color: #333; opacity: 1; }
|
||||
.al-blue { background: #3498db; }
|
||||
.al-yellow { background: #f1c40f; color: #333; }
|
||||
.al-yellow .n, .al-yellow .t { color: #333; }
|
||||
.al-orange { background: #f39c12; }
|
||||
.al-red { background: #e74c3c; }
|
||||
.bar-row { margin-bottom: 0.4375rem; }
|
||||
.bar-row .meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 0.15rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.bar-track {
|
||||
height: 7px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bar-fill { height: 100%; background: #f59e0b; border-radius: 4px; }
|
||||
.card-low-stock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
.card-low-stock .card-bd {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.card-low-stock .low-stock-grid {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
gap: 0.75rem;
|
||||
min-height: 9.5rem;
|
||||
}
|
||||
.grid .card-low-stock.stock-tall .low-stock-grid {
|
||||
min-height: 100%;
|
||||
gap: 1rem;
|
||||
}
|
||||
.card-low-stock .bar-row {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-bottom: 0;
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
.grid .card-low-stock.stock-tall .bar-row {
|
||||
min-height: 3.25rem;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.card-low-stock .bar-row .meta { margin-bottom: 0.3rem; }
|
||||
.grid .card-low-stock.stock-tall .bar-row .meta {
|
||||
margin-bottom: 0.45rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.card-low-stock .bar-track {
|
||||
flex: 1 1 auto;
|
||||
height: auto;
|
||||
min-height: 12px;
|
||||
max-height: 2.25rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.grid .card-low-stock.stock-tall .bar-track {
|
||||
min-height: 16px;
|
||||
max-height: 3.25rem;
|
||||
}
|
||||
.card-low-stock .bar-fill { border-radius: 6px; }
|
||||
.stock-pair.grid2 {
|
||||
align-items: stretch;
|
||||
}
|
||||
.stock-pair.grid2 .card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
.stock-pair.grid2 .card-stock-alert .card-bd {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.stock-pair.grid2 .card-stock-alert .alert-levels {
|
||||
flex: 1;
|
||||
align-items: stretch;
|
||||
min-height: 9.5rem;
|
||||
}
|
||||
.stock-pair.grid2 .card-stock-alert .alert-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
padding: 0.625rem 0.3rem;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.stock-pair.grid2 .card-stock-alert .alert-box .n {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
.stock-pair.grid2 .card-stock-alert .alert-box .alert-bags {
|
||||
font-size: 0.625rem;
|
||||
margin-top: 0.45rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
.stock-pair.grid2 .card-stock-alert .alert-box .alert-bags li {
|
||||
white-space: normal;
|
||||
line-height: 1.25;
|
||||
}
|
||||
23
app/Views/home/_dashboard_gov_portal_stock_cards_pair.php
Normal file
23
app/Views/home/_dashboard_gov_portal_stock_cards_pair.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** @var list<array{name:string,percent:int}> $lowStock */
|
||||
?>
|
||||
<div class="card card-stock-alert">
|
||||
<div class="card-hd"><span><i class="fa-solid fa-triangle-exclamation"></i> 재고 경보</span></div>
|
||||
<div class="card-bd">
|
||||
<?= view('home/_dashboard_gov_portal_stock_alert_levels', ['stockAlerts' => $stockAlerts]) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-low-stock">
|
||||
<div class="card-hd"><span><i class="fa-solid fa-box-open"></i> 부족 재고</span></div>
|
||||
<div class="card-bd">
|
||||
<div class="low-stock-grid">
|
||||
<?php foreach ($lowStock as $item): ?>
|
||||
<div class="bar-row">
|
||||
<div class="meta"><span><?= esc($item['name']) ?></span><span><?= esc((string) $item['percent']) ?>%</span></div>
|
||||
<div class="bar-track"><div class="bar-fill" style="width:<?= (int) $item['percent'] ?>%"></div></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
89
app/Views/home/_dashboard_gov_portal_strip_home_inner.php
Normal file
89
app/Views/home/_dashboard_gov_portal_strip_home_inner.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** gov-portal-strip 메인 대시보드 본문 */
|
||||
?>
|
||||
<div class="kpi-strip">
|
||||
<div class="kpi-card">
|
||||
<div class="ico"><i class="fa-solid fa-boxes-stacked"></i></div>
|
||||
<div><div class="n ok">양호</div><div class="l">봉투 재고 상태</div></div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="ico"><i class="fa-solid fa-inbox"></i></div>
|
||||
<div><div class="n">12</div><div class="l">미처리 구매신청</div></div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="ico"><i class="fa-solid fa-user-check"></i></div>
|
||||
<div><div class="n">4</div><div class="l">승인 대기</div></div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="ico"><i class="fa-solid fa-location-dot"></i></div>
|
||||
<div><div class="n" style="font-size:.95rem;color:var(--text-dark);"><?= esc($lgLabel) ?></div><div class="l">담당 지자체</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="hero">
|
||||
<div class="hero-hd">
|
||||
<span><i class="fa-solid fa-map"></i> GIS 통합 현황판</span>
|
||||
<a href="<?= base_url('bag/flow') ?>">수불 조회 ></a>
|
||||
</div>
|
||||
<div class="hero-body">
|
||||
<div class="hero-map">
|
||||
<?= view('home/_dashboard_gov_portal_map_panel', [
|
||||
'mapId' => 'govPortalStripMap',
|
||||
'mapFill' => true,
|
||||
'lgLabel' => $lgLabel,
|
||||
'govMapPanel' => $govMapPanel,
|
||||
]) ?>
|
||||
</div>
|
||||
<div class="hero-tl">
|
||||
<?php foreach ($timeline as $ev): ?>
|
||||
<div class="item">
|
||||
<span class="time"><?= esc($ev['time']) ?></span>
|
||||
<span class="txt"><?= esc($ev['text']) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid2 stock-pair">
|
||||
<?= view('home/_dashboard_gov_portal_stock_cards_pair', ['lowStock' => $lowStock, 'stockAlerts' => $stockAlerts]) ?>
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<div class="card-hd"><i class="fa-regular fa-envelope"></i> 메시지</div>
|
||||
<div class="card-bd">
|
||||
<?php foreach ($notices as $n): ?>
|
||||
<div class="notice-row">
|
||||
<span class="notice-t"><?= esc($n['title']) ?></span>
|
||||
<span class="notice-d"><?= esc($n['date']) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?= view('home/_dashboard_gov_portal_menu_search', [
|
||||
'variant' => 'inline',
|
||||
'inputId' => 'menuSearch',
|
||||
'menuSearchOptions' => $menuSearchOptions,
|
||||
]) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-hd"><i class="fa-solid fa-chart-column"></i> 최근 7일 신청 · 바로가기</div>
|
||||
<div class="card-bd">
|
||||
<div class="bars">
|
||||
<?php $maxReq = max($weeklyRequests); foreach ($weeklyRequests as $idx => $v): $h = (int) round(($v / $maxReq) * 100); ?>
|
||||
<div class="col"><span><?= esc((string) $v) ?></span><div class="bar" style="height:<?= $h ?>%"></div><span>D<?= 6 - $idx ?></span></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="quick-list" style="margin-top:.75rem;">
|
||||
<?php foreach ($quickLinks as $link): ?>
|
||||
<a href="<?= esc($link['url']) ?>">
|
||||
<i class="fa-solid <?= esc($link['icon'], 'attr') ?>"></i>
|
||||
<?= esc($link['label']) ?>
|
||||
<span><?= esc($link['desc']) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
81
app/Views/home/_dashboard_gov_portal_strip_layout.php
Normal file
81
app/Views/home/_dashboard_gov_portal_strip_layout.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* gov-portal-strip 공통 페이지 셸 (상단 호버 메뉴 + 프로필 + 가로 MY MENU + 본문)
|
||||
*
|
||||
* @var string $stripInnerView 본문 partial 경로 (home/...)
|
||||
* @var bool $stripIncludeWorkCss NDMS 작업화면 CSS 포함 여부
|
||||
*/
|
||||
helper('admin');
|
||||
$govPortalNavPartial = gov_portal_nav_partial_vars(get_defined_vars());
|
||||
$activeVariant = $activeVariant ?? 'strip';
|
||||
$stripInnerView = (string) ($stripInnerView ?? '');
|
||||
$stripIncludeWorkCss = ! empty($stripIncludeWorkCss);
|
||||
$stripShowProfileLinks = ($stripShowProfileLinks ?? true) !== false;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?= view('home/_dashboard_gov_portal_head') ?>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"/>
|
||||
<?php if (! $stripIncludeWorkCss): ?>
|
||||
<?= view('home/_dashboard_gov_portal_map_leaflet_assets') ?>
|
||||
<?php endif; ?>
|
||||
<style>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_brand_css.php'; ?>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_topnav_css.php'; ?>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_strip_styles.php'; ?>
|
||||
<?php if (! $stripIncludeWorkCss): ?>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_map_css.php'; ?>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_menu_search_css.php'; ?>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_stock_cards_css.php'; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($stripIncludeWorkCss): ?>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_workpage_css.php'; ?>
|
||||
.page.strip-work-page .work-main { padding:0; background:transparent; }
|
||||
<?php endif; ?>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="portal-header">
|
||||
<div class="portal-header-inner">
|
||||
<?= view('home/_dashboard_gov_portal_brand', ['brandHref' => base_url('dashboard/gov-portal-strip')]) ?>
|
||||
<?= view('home/_dashboard_gov_portal_topnav_hover', $govPortalNavPartial) ?>
|
||||
<?= view('home/_dashboard_gov_portal_header_utils', [
|
||||
'activeVariant' => $activeVariant,
|
||||
'portalVariants' => $portalVariants,
|
||||
'mbName' => $mbName,
|
||||
'levelName' => $levelName,
|
||||
'lgLabel' => $lgLabel,
|
||||
]) ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page<?= $stripIncludeWorkCss ? ' strip-work-page' : '' ?>">
|
||||
<div class="profile-inline">
|
||||
<div>
|
||||
<div class="name"><?= esc($mbName) ?>님, 환영합니다</div>
|
||||
<div class="sub"><?= esc($lgLabel) ?> · <?= esc($levelName) ?> · 최근접속 <?= date('Y.m.d H:i') ?></div>
|
||||
</div>
|
||||
<?php if ($stripShowProfileLinks): ?>
|
||||
<div style="display:flex;gap:.35rem;flex-wrap:wrap;">
|
||||
<a href="<?= base_url('dashboard/simple') ?>">마이페이지</a>
|
||||
<a href="<?= base_url('logout') ?>">로그아웃</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= view('home/_dashboard_gov_portal_strip_my_menu', $govPortalNavPartial) ?>
|
||||
|
||||
<?php if ($stripInnerView !== ''): ?>
|
||||
<div class="<?= $stripIncludeWorkCss ? 'work-main' : 'strip-page-body' ?>">
|
||||
<?= view($stripInnerView, get_defined_vars()) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= view('home/_dashboard_gov_portal_nav_script_base', $govPortalNavPartial) ?>
|
||||
<?= view('home/_dashboard_gov_portal_font_zoom_script') ?>
|
||||
</body>
|
||||
</html>
|
||||
41
app/Views/home/_dashboard_gov_portal_strip_my_menu.php
Normal file
41
app/Views/home/_dashboard_gov_portal_strip_my_menu.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* gov-portal-strip 가로 MY MENU (활성 대메뉴 하위 소메뉴 칩)
|
||||
*
|
||||
* @var list<array> $govNavItems
|
||||
* @var int $govActiveParentIdx
|
||||
* @var string $govActiveChildHref
|
||||
*/
|
||||
$activeParent = $govNavItems[$govActiveParentIdx] ?? $govNavItems[0] ?? null;
|
||||
if ($activeParent === null) {
|
||||
return;
|
||||
}
|
||||
$children = $activeParent['children'] ?? [];
|
||||
if ($children === []) {
|
||||
return;
|
||||
}
|
||||
$activeChildHref = strtolower(ltrim((string) ($govActiveChildHref ?? ''), '/'));
|
||||
$sectionTitle = (string) ($activeParent['name'] ?? 'MY MENU');
|
||||
?>
|
||||
<nav class="strip-my-menu" aria-label="<?= esc($sectionTitle) ?> 하위 메뉴">
|
||||
<span class="strip-my-menu-title">MY MENU · <?= esc($sectionTitle) ?></span>
|
||||
<div class="strip-my-menu-chips">
|
||||
<?php foreach ($children as $ci => $child): ?>
|
||||
<?php if (($child['href'] ?? '') === ''): ?>
|
||||
<span class="menu-sub" style="opacity:.55;font-size:.75rem;padding:.35rem .5rem;"><?= esc($child['name']) ?></span>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$childHref = strtolower(ltrim((string) $child['href'], '/'));
|
||||
$isActive = $activeChildHref !== ''
|
||||
? ($childHref === $activeChildHref)
|
||||
: ($ci === 0);
|
||||
?>
|
||||
<a href="<?= esc($child['url']) ?>" class="<?= $isActive ? 'active' : '' ?>">
|
||||
<span class="menu-ico"><?= $isActive ? '×' : '+' ?></span>
|
||||
<?= esc($child['name']) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</nav>
|
||||
67
app/Views/home/_dashboard_gov_portal_strip_styles.php
Normal file
67
app/Views/home/_dashboard_gov_portal_strip_styles.php
Normal file
@@ -0,0 +1,67 @@
|
||||
:root { --navy:#1a2b4b; --navy-deep:#002b4e; --blue:#007bff; --blue-menu:#4a69bd; --teal:#009688; --bg:#f0f4f8; --border:#dde4ec; --text:#444; --text-dark:#222; --muted:#888; --font-scale:1; }
|
||||
* { box-sizing:border-box; margin:0; padding:0; }
|
||||
html { font-size:calc(14px * var(--font-scale)); -webkit-text-size-adjust:100%; }
|
||||
body { font-family:'Pretendard','Malgun Gothic','Noto Sans KR',sans-serif; font-size:.8125rem; font-weight:400; line-height:1.45; letter-spacing:-.02em; color:var(--text); background:var(--bg); min-height:100vh; -webkit-font-smoothing:antialiased; }
|
||||
.page { padding:.875rem 1rem 1.25rem; max-width:1400px; margin:0 auto; }
|
||||
.profile-inline { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; background:#4a5568; color:#fff; padding:1rem; border-radius:12px; margin-bottom:.875rem; }
|
||||
.profile-inline .name { font-size:1rem; font-weight:700; }
|
||||
.profile-inline .sub { font-size:.6875rem; opacity:.8; margin-top:.15rem; }
|
||||
.profile-inline a { color:#fff; font-size:.75rem; font-weight:600; text-decoration:none; border:1px solid rgba(255,255,255,.4); padding:.3rem .55rem; border-radius:4px; }
|
||||
.strip-my-menu {
|
||||
display:flex; flex-wrap:wrap; align-items:center; gap:.35rem;
|
||||
padding:.5rem .75rem; margin-bottom:.875rem;
|
||||
background:#fff; border:1px solid var(--border); border-radius:10px;
|
||||
box-shadow:0 1px 3px rgba(26,43,75,.05);
|
||||
}
|
||||
.strip-my-menu .strip-my-menu-title {
|
||||
font-size:.6875rem; font-weight:700; color:var(--navy);
|
||||
margin-right:.35rem; letter-spacing:.04em; white-space:nowrap;
|
||||
}
|
||||
.strip-my-menu .strip-my-menu-chips { display:flex; flex-wrap:wrap; gap:.35rem; flex:1; }
|
||||
.strip-my-menu a {
|
||||
display:inline-flex; align-items:center; gap:.25rem;
|
||||
padding:.4rem .65rem; border-radius:10px;
|
||||
background:var(--blue-menu); color:#fff; text-decoration:none;
|
||||
font-size:.8125rem; font-weight:600;
|
||||
border:1px solid rgba(255,255,255,.22);
|
||||
}
|
||||
.strip-my-menu a:hover { filter:brightness(1.06); }
|
||||
.strip-my-menu a.active { background:#3d5a9e; border-color:rgba(255,255,255,.4); }
|
||||
.strip-my-menu a .menu-ico { font-size:.625rem; width:.75rem; text-align:center; }
|
||||
.kpi-strip { display:grid; grid-template-columns:repeat(4,1fr); gap:.75rem; margin-bottom:.875rem; }
|
||||
@media(max-width:900px){.kpi-strip{grid-template-columns:repeat(2,1fr)}}
|
||||
.kpi-card { background:#fff; border-radius:12px; border:1px solid var(--border); padding:.75rem 1rem; display:flex; align-items:center; gap:.75rem; box-shadow:0 1px 3px rgba(26,43,75,.05); }
|
||||
.kpi-card .ico { width:40px; height:40px; border-radius:10px; background:#eef6ff; color:var(--blue); display:flex; align-items:center; justify-content:center; font-size:1rem; }
|
||||
.kpi-card .n { font-size:1.35rem; font-weight:700; color:#2563eb; line-height:1.1; }
|
||||
.kpi-card .n.ok { font-size:1.1rem; color:#10b981; }
|
||||
.kpi-card .l { font-size:.6875rem; color:var(--muted); font-weight:600; margin-top:.1rem; }
|
||||
.hero { background:#fff; border-radius:12px; border:1px solid var(--border); overflow:hidden; margin-bottom:.875rem; box-shadow:0 1px 3px rgba(26,43,75,.06); }
|
||||
.hero-hd { display:flex; justify-content:space-between; align-items:center; padding:.6rem .875rem; border-bottom:1px solid var(--border); font-size:1rem; font-weight:700; color:var(--text-dark); }
|
||||
.hero-hd a { font-size:.6875rem; font-weight:600; color:#fff; background:var(--blue); padding:.25rem .5rem; border-radius:3px; text-decoration:none; }
|
||||
.hero-body { display:grid; grid-template-columns:1fr 200px; min-height:200px; }
|
||||
@media(max-width:800px){.hero-body{grid-template-columns:1fr}}
|
||||
.hero-map { position:relative; min-height:220px; overflow:hidden; }
|
||||
.hero-tl { background:var(--navy-deep); color:#fff; padding:.4rem; overflow-y:auto; max-height:220px; }
|
||||
.hero-tl .item { padding:.4rem .35rem; border-bottom:1px solid rgba(255,255,255,.1); font-size:.75rem; }
|
||||
.hero-tl .time { font-weight:700; font-size:.8125rem; display:block; }
|
||||
.hero-tl .txt { color:#4fc3f7; font-weight:600; }
|
||||
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:.875rem; }
|
||||
@media(max-width:900px){.grid2{grid-template-columns:1fr}}
|
||||
.card { background:#fff; border-radius:12px; border:1px solid var(--border); box-shadow:0 1px 3px rgba(26,43,75,.05); overflow:hidden; }
|
||||
.card-hd { padding:.6rem .875rem; border-bottom:1px solid var(--border); font-weight:700; font-size:1rem; color:var(--text-dark); }
|
||||
.card-hd i { color:var(--blue); margin-right:.3rem; }
|
||||
.card-bd { padding:.875rem 1rem; }
|
||||
.notice-t { font-size:.8125rem; font-weight:600; color:var(--text-dark); }
|
||||
.notice-d { font-size:.6875rem; color:var(--blue); background:#eef6ff; padding:.1rem .35rem; border-radius:2px; font-weight:600; margin-left:.35rem; }
|
||||
.notice-row { padding:.4rem 0; border-bottom:1px dashed #e8edf2; }
|
||||
.bars { display:flex; align-items:flex-end; gap:4px; height:56px; }
|
||||
.bars .col { flex:1; text-align:center; font-size:.625rem; color:var(--muted); font-weight:500; display:flex; flex-direction:column; justify-content:flex-end; gap:2px; }
|
||||
.bars .bar { background:linear-gradient(180deg,#2563eb,#60a5fa); border-radius:3px 3px 0 0; min-height:4px; width:100%; }
|
||||
.quick-list a { display:flex; align-items:center; gap:.5rem; padding:.4rem 0; text-decoration:none; color:var(--text); border-bottom:1px solid #f1f5f9; font-size:.8125rem; font-weight:600; }
|
||||
.quick-list a:last-child { border-bottom:none; }
|
||||
.quick-list a:hover { color:var(--blue); }
|
||||
.quick-list span { font-size:.6875rem; font-weight:400; color:var(--muted); margin-left:auto; }
|
||||
.search-inline { display:flex; gap:.5rem; background:var(--teal); padding:.65rem .75rem; border-radius:10px; color:#fff; align-items:center; margin-top:.5rem; }
|
||||
.search-inline input { flex:1; border:none; border-radius:4px; padding:.4rem .5rem; font-size:.8125rem; font-family:inherit; }
|
||||
.search-inline label { font-weight:700; font-size:.8125rem; white-space:nowrap; }
|
||||
.stock-pair.grid2 { margin-bottom:.875rem; }
|
||||
42
app/Views/home/_dashboard_gov_portal_topnav_click.php
Normal file
42
app/Views/home/_dashboard_gov_portal_topnav_click.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* 대메뉴 클릭 → 좌측 MY MENU에 소메뉴 표시 (gov-portal 기본)
|
||||
*
|
||||
* @var list<array> $govNavItems
|
||||
* @var int $govActiveParentIdx
|
||||
* @var string $govCurrentPath
|
||||
* @var list<string> $govDashboardAliases
|
||||
*/
|
||||
helper('admin');
|
||||
?>
|
||||
<nav class="portal-top-nav" id="portalTopNavClick" aria-label="대분류 메뉴">
|
||||
<?php foreach ($govNavItems as $pIdx => $item): ?>
|
||||
<?php
|
||||
$isActive = ($pIdx === $govActiveParentIdx);
|
||||
$hasChildren = ! empty($item['hasChildren']);
|
||||
if ($hasChildren):
|
||||
?>
|
||||
<div class="portal-nav-item">
|
||||
<button type="button"
|
||||
class="portal-nav-trigger <?= $isActive ? 'is-active' : '' ?>"
|
||||
data-parent-idx="<?= (int) $pIdx ?>"
|
||||
aria-expanded="<?= $isActive ? 'true' : 'false' ?>">
|
||||
<?= esc($item['name']) ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php elseif ($item['href'] !== ''): ?>
|
||||
<div class="portal-nav-item">
|
||||
<a href="<?= esc($item['url']) ?>"
|
||||
class="portal-nav-link <?= $isActive ? 'is-active' : '' ?>"
|
||||
data-parent-idx="<?= (int) $pIdx ?>">
|
||||
<?= esc($item['name']) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="portal-nav-item">
|
||||
<span class="portal-nav-trigger" style="opacity:.5;cursor:default;"><?= esc($item['name']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
171
app/Views/home/_dashboard_gov_portal_topnav_css.php
Normal file
171
app/Views/home/_dashboard_gov_portal_topnav_css.php
Normal file
@@ -0,0 +1,171 @@
|
||||
/* 상단 메뉴바 — 기본·변형 동일 높이 */
|
||||
.portal-header {
|
||||
background: var(--navy);
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.portal-header-inner {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
padding: 0 1rem;
|
||||
min-height: 48px;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.portal-top-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
gap: 0 0.25rem;
|
||||
min-height: 48px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.portal-nav-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
}
|
||||
.portal-nav-link,
|
||||
.portal-nav-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 0.625rem;
|
||||
min-height: 48px;
|
||||
box-sizing: border-box;
|
||||
color: rgba(255,255,255,.9);
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
border-bottom: 4px solid transparent;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
letter-spacing: -0.02em;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.portal-nav-link:hover,
|
||||
.portal-nav-trigger:hover {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
.portal-nav-link.is-active,
|
||||
.portal-nav-trigger.is-active {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.portal-nav-dropdown {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
z-index: 300;
|
||||
margin-top: -2px;
|
||||
padding-top: 4px;
|
||||
min-width: 12rem;
|
||||
display: none;
|
||||
}
|
||||
.portal-nav-item:hover .portal-nav-dropdown,
|
||||
.portal-nav-item:focus-within .portal-nav-dropdown {
|
||||
display: block;
|
||||
}
|
||||
.portal-nav-dropdown-panel {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 16px rgba(26,43,75,.12);
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
.portal-nav-dropdown-panel a {
|
||||
display: block;
|
||||
padding: 0.45rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.portal-nav-dropdown-panel a:hover {
|
||||
background: #eff6ff;
|
||||
color: var(--blue-ui);
|
||||
}
|
||||
.portal-nav-dropdown-panel a.is-active {
|
||||
background: #eff6ff;
|
||||
color: var(--blue-ui);
|
||||
font-weight: 700;
|
||||
}
|
||||
.portal-nav-dropdown-panel .no-link {
|
||||
display: block;
|
||||
padding: 0.45rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
color: #94a3b8;
|
||||
cursor: default;
|
||||
}
|
||||
.portal-header-utils {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255,255,255,.92);
|
||||
flex-wrap: wrap;
|
||||
min-height: 48px;
|
||||
}
|
||||
.portal-header-utils .user-line {
|
||||
max-width: 11rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.portal-header-utils .extend-btn {
|
||||
background: var(--blue-ui);
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.portal-header-utils .font-zoom {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
font-size: 0.6875rem;
|
||||
border: 1px solid rgba(255,255,255,.25);
|
||||
border-radius: 3px;
|
||||
padding: 0.125rem 0.25rem;
|
||||
background: rgba(0,0,0,.12);
|
||||
}
|
||||
.portal-header-utils .font-zoom button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
.portal-header-utils .util-ico {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
opacity: 0.88;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
.portal-header-utils .util-ico:hover { opacity: 1; color: #fff; }
|
||||
.variant-nav {
|
||||
display: inline-flex;
|
||||
gap: 0.125rem;
|
||||
padding: 0.125rem;
|
||||
background: rgba(0,0,0,.15);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.variant-nav a {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.45rem;
|
||||
border-radius: 3px;
|
||||
color: rgba(255,255,255,.78);
|
||||
text-decoration: none;
|
||||
}
|
||||
.variant-nav a.on, .variant-nav a:hover { background: rgba(255,255,255,.18); color: #fff; }
|
||||
59
app/Views/home/_dashboard_gov_portal_topnav_hover.php
Normal file
59
app/Views/home/_dashboard_gov_portal_topnav_hover.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* 대메뉴 호버 시 소메뉴 드롭다운 (gov-portal-strip)
|
||||
*
|
||||
* @var list<array> $govNavItems
|
||||
* @var int $govActiveParentIdx
|
||||
* @var string $govCurrentPath
|
||||
* @var list<string> $govDashboardAliases
|
||||
*/
|
||||
helper('admin');
|
||||
?>
|
||||
<nav class="portal-top-nav" aria-label="대분류 메뉴">
|
||||
<?php foreach ($govNavItems as $pIdx => $item): ?>
|
||||
<?php
|
||||
$isActive = ($pIdx === $govActiveParentIdx);
|
||||
$parentHref = $item['href'] ?? '';
|
||||
$hasChildren = ! empty($item['hasChildren']);
|
||||
$activeChild = null;
|
||||
if ($hasChildren && ! empty($siteNavTree[$pIdx])) {
|
||||
$activeChild = menu_active_child_for_parent($siteNavTree[$pIdx], $govCurrentPath, $govDashboardAliases);
|
||||
}
|
||||
?>
|
||||
<div class="portal-nav-item">
|
||||
<?php if ($hasChildren): ?>
|
||||
<?php if ($parentHref !== ''): ?>
|
||||
<a href="<?= esc($item['url']) ?>" class="portal-nav-link <?= $isActive ? 'is-active' : '' ?>">
|
||||
<?= esc($item['name']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="portal-nav-trigger <?= $isActive ? 'is-active' : '' ?>" tabindex="0"><?= esc($item['name']) ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="portal-nav-dropdown">
|
||||
<div class="portal-nav-dropdown-panel">
|
||||
<?php foreach ($item['children'] as $child): ?>
|
||||
<?php if ($child['href'] !== ''): ?>
|
||||
<?php
|
||||
$childActive = $activeChild !== null
|
||||
&& (int) ($activeChild->mm_idx ?? 0) === (int) ($child['idx'] ?? 0);
|
||||
?>
|
||||
<a href="<?= esc($child['url']) ?>" class="<?= $childActive ? 'is-active' : '' ?>">
|
||||
<?= esc($child['name']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="no-link" title="메뉴 링크 미설정"><?= esc($child['name']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($parentHref !== ''): ?>
|
||||
<a href="<?= esc($item['url']) ?>" class="portal-nav-link <?= $isActive ? 'is-active' : '' ?>">
|
||||
<?= esc($item['name']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="portal-nav-trigger" style="opacity:.5;"><?= esc($item['name']) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
17
app/Views/home/_dashboard_gov_portal_variant_nav.php
Normal file
17
app/Views/home/_dashboard_gov_portal_variant_nav.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** @var array<int, array{label: string, url: string}> $portalVariants */
|
||||
/** @var string $activeVariant base|alt|strip */
|
||||
?>
|
||||
<nav class="variant-nav" aria-label="포털 시안 전환">
|
||||
<?php foreach ($portalVariants as $v):
|
||||
$key = match ($v['label']) {
|
||||
'기본' => 'base',
|
||||
'변형' => 'strip',
|
||||
default => '',
|
||||
};
|
||||
$isActive = ($activeVariant ?? '') === $key;
|
||||
?>
|
||||
<a href="<?= esc($v['url']) ?>" class="<?= $isActive ? 'on' : '' ?>"><?= esc($v['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
192
app/Views/home/_dashboard_gov_portal_workpage_css.php
Normal file
192
app/Views/home/_dashboard_gov_portal_workpage_css.php
Normal file
@@ -0,0 +1,192 @@
|
||||
.main.work-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
background: #f5f7fa;
|
||||
padding: 0.75rem 1rem 1.5rem;
|
||||
}
|
||||
.work-breadcrumb {
|
||||
font-size: 0.6875rem;
|
||||
color: #888;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.work-breadcrumb a { color: #666; text-decoration: none; }
|
||||
.work-breadcrumb a:hover { text-decoration: underline; }
|
||||
.work-breadcrumb .sep { margin: 0 0.35rem; color: #bbb; }
|
||||
.work-page-hd {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.work-page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.work-page-title .fav {
|
||||
color: #f59e0b;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.work-actions { display: flex; flex-wrap: wrap; gap: 0.375rem; }
|
||||
.btn-portal {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 3px;
|
||||
border: 1px solid transparent;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.btn-portal-primary {
|
||||
background: #00205b;
|
||||
color: #fff;
|
||||
border-color: #00205b;
|
||||
}
|
||||
.btn-portal-primary:hover { background: #003080; }
|
||||
.btn-portal-secondary {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
border-color: #c5cdd8;
|
||||
}
|
||||
.btn-portal-secondary:hover { background: #f8fafc; }
|
||||
.btn-portal-search {
|
||||
background: #3366ff;
|
||||
color: #fff;
|
||||
border-color: #3366ff;
|
||||
}
|
||||
.btn-portal-search:hover { background: #2952cc; }
|
||||
.btn-portal-reset {
|
||||
background: #fff;
|
||||
color: #555;
|
||||
border-color: #c5cdd8;
|
||||
}
|
||||
.search-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #d1d5db;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.search-panel table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.search-panel th {
|
||||
width: 10%;
|
||||
min-width: 5.5rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.45rem 0.65rem;
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.search-panel td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: #fff;
|
||||
}
|
||||
.search-panel input,
|
||||
.search-panel select {
|
||||
width: 100%;
|
||||
max-width: 14rem;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.3rem 0.45rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: inherit;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.search-panel-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.35rem;
|
||||
padding: 0.5rem 0.65rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
background: #fafbfc;
|
||||
}
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.table-toolbar .total strong { color: #3366ff; font-weight: 700; }
|
||||
.table-toolbar .tools { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
|
||||
.table-toolbar select {
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.25rem 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.portal-data-table-wrap {
|
||||
background: #fff;
|
||||
border: 1px solid #d1d5db;
|
||||
overflow: auto;
|
||||
}
|
||||
.portal-data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.portal-data-table thead th {
|
||||
background: #e8eef5;
|
||||
color: #00205b;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0.4rem;
|
||||
border: 1px solid #d1d5db;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.portal-data-table tbody td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.4rem 0.5rem;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
.portal-data-table tbody tr:nth-child(even) { background: #f9fafb; }
|
||||
.portal-data-table tbody tr:hover { background: #eef6ff; }
|
||||
.portal-data-table tbody tr.is-selected { background: #dbeafe; }
|
||||
.portal-data-table tbody td.text-left { text-align: left; }
|
||||
.portal-data-table tbody td a { color: #3366ff; text-decoration: none; }
|
||||
.portal-data-table tbody td a:hover { text-decoration: underline; }
|
||||
.portal-data-table .empty-row td {
|
||||
padding: 2rem;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
}
|
||||
.detail-section {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.detail-section-hd {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
margin-bottom: 0.4rem;
|
||||
padding-bottom: 0.35rem;
|
||||
border-bottom: 2px solid #00205b;
|
||||
}
|
||||
.flash-banner {
|
||||
margin-bottom: 0.65rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.flash-banner.ok { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; }
|
||||
.flash-banner.err { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
|
||||
224
app/Views/home/_gov_portal_code_kinds_body.php
Normal file
224
app/Views/home/_gov_portal_code_kinds_body.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/** @var list<object> $codeKinds */
|
||||
/** @var array<int,int> $countMap */
|
||||
/** @var bool $canManageKinds */
|
||||
/** @var bool $canManageDetails */
|
||||
/** @var object|null $selectedKind */
|
||||
/** @var list<object> $detailList */
|
||||
/** @var array<int,bool> $rowCanEdit */
|
||||
/** @var int $totalCount */
|
||||
/** @var array<string,string> $filters */
|
||||
/** @var string $pageBaseUrl */
|
||||
|
||||
$canManageKinds = ! empty($canManageKinds);
|
||||
$canManageDetails = ! empty($canManageDetails);
|
||||
$selectedKindId = (int) ($selectedKind->ck_idx ?? 0);
|
||||
$filters = is_array($filters ?? null) ? $filters : [];
|
||||
$activeVariant = (string) ($activeVariant ?? 'base');
|
||||
$pageBaseUrl = (string) ($pageBaseUrl ?? site_url(gov_portal_code_kinds_portal_path($activeVariant)));
|
||||
|
||||
$buildUrl = static function (array $extra = []) use ($filters, $selectedKindId, $pageBaseUrl): string {
|
||||
$q = array_merge([
|
||||
'search' => '1',
|
||||
'q_code' => $filters['q_code'] ?? '',
|
||||
'q_name' => $filters['q_name'] ?? '',
|
||||
'q_state' => $filters['q_state'] ?? '',
|
||||
'ck_idx' => $selectedKindId > 0 ? (string) $selectedKindId : '',
|
||||
], $extra);
|
||||
foreach ($q as $k => $v) {
|
||||
if ($v === '' || $v === null) {
|
||||
unset($q[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
return $pageBaseUrl . '?' . http_build_query($q);
|
||||
};
|
||||
?>
|
||||
<nav class="work-breadcrumb" aria-label="breadcrumb">
|
||||
<a href="<?= base_url($activeVariant === 'strip' ? 'dashboard/gov-portal-strip' : 'dashboard/gov-portal') ?>">Home</a>
|
||||
<span class="sep">></span>
|
||||
<span>시스템관리</span>
|
||||
<span class="sep">></span>
|
||||
<span>기본 코드관리(포털 UI 시안)</span>
|
||||
</nav>
|
||||
|
||||
<div class="work-page-hd">
|
||||
<h1 class="work-page-title">
|
||||
<span>기본 코드관리</span>
|
||||
<span style="font-size:0.75rem;font-weight:600;color:#666;">포털 UI 시안</span>
|
||||
<i class="fa-regular fa-star fav" title="즐겨찾기(목업)" aria-hidden="true"></i>
|
||||
</h1>
|
||||
<div class="work-actions">
|
||||
<?php if ($canManageKinds): ?>
|
||||
<a href="<?= base_url('admin/code-kinds/create') ?>" class="btn-portal btn-portal-primary">
|
||||
<i class="fa-solid fa-plus"></i> 등록
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a href="<?= base_url('bag/code-kinds') ?>" class="btn-portal btn-portal-secondary" title="운영 중인 기본 코드관리 화면">
|
||||
<i class="fa-solid fa-table-columns"></i> 운영 화면 (/bag/code-kinds)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="flash-banner ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="flash-banner err"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="get" action="<?= esc($pageBaseUrl, 'attr') ?>" class="search-panel">
|
||||
<input type="hidden" name="search" value="1"/>
|
||||
<?php if ($selectedKindId > 0): ?>
|
||||
<input type="hidden" name="ck_idx" value="<?= $selectedKindId ?>"/>
|
||||
<?php endif; ?>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>코드</th>
|
||||
<td><input type="text" name="q_code" value="<?= esc($filters['q_code'] ?? '') ?>" placeholder="종류 코드"/></td>
|
||||
<th>코드명</th>
|
||||
<td><input type="text" name="q_name" value="<?= esc($filters['q_name'] ?? '') ?>" placeholder="종류명"/></td>
|
||||
<th>사용여부</th>
|
||||
<td>
|
||||
<select name="q_state">
|
||||
<option value="">전체</option>
|
||||
<option value="1" <?= ($filters['q_state'] ?? '') === '1' ? 'selected' : '' ?>>사용</option>
|
||||
<option value="0" <?= ($filters['q_state'] ?? '') === '0' ? 'selected' : '' ?>>미사용</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="search-panel-foot">
|
||||
<a href="<?= esc($pageBaseUrl, 'attr') ?>" class="btn-portal btn-portal-reset">
|
||||
<i class="fa-solid fa-rotate-left"></i> 초기화
|
||||
</a>
|
||||
<button type="submit" class="btn-portal btn-portal-search">
|
||||
<i class="fa-solid fa-magnifying-glass"></i> 검색
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section aria-labelledby="codeKindTableLabel">
|
||||
<div class="table-toolbar">
|
||||
<div class="total">전체 <strong><?= number_format($totalCount) ?></strong> 건</div>
|
||||
<div class="tools">
|
||||
<span class="text-gray-500">코드 종류</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="portal-data-table-wrap">
|
||||
<table class="portal-data-table" id="codeKindTableLabel">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12">번호</th>
|
||||
<th class="w-20">코드</th>
|
||||
<th>코드명</th>
|
||||
<th class="w-20">세부코드</th>
|
||||
<th class="w-16">사용여부</th>
|
||||
<th class="w-28">등록일</th>
|
||||
<?php if ($canManageKinds): ?>
|
||||
<th class="w-24">작업</th>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($codeKinds !== []): ?>
|
||||
<?php $i = 0; foreach ($codeKinds as $row): $i++; ?>
|
||||
<?php $isSelected = (int) $row->ck_idx === $selectedKindId; ?>
|
||||
<tr class="<?= $isSelected ? 'is-selected' : '' ?>"
|
||||
onclick="window.location.href='<?= esc($buildUrl(['ck_idx' => (string) $row->ck_idx]), 'attr') ?>'"
|
||||
style="cursor:pointer">
|
||||
<td><?= (string) $i ?></td>
|
||||
<td class="font-mono"><?= esc($row->ck_code) ?></td>
|
||||
<td class="text-left"><?= esc($row->ck_name) ?></td>
|
||||
<td><?= (int) ($countMap[$row->ck_idx] ?? 0) ?>개</td>
|
||||
<td><?= (int) ($row->ck_state ?? 0) === 1 ? '사용' : '미사용' ?></td>
|
||||
<td><?= esc($row->ck_regdate ?? '') ?></td>
|
||||
<?php if ($canManageKinds): ?>
|
||||
<td onclick="event.stopPropagation()">
|
||||
<a href="<?= base_url('admin/code-kinds/edit/' . (int) $row->ck_idx) ?>">수정</a>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr class="empty-row">
|
||||
<td colspan="<?= $canManageKinds ? '7' : '6' ?>">등록된 코드 종류가 없습니다.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($selectedKind !== null): ?>
|
||||
<section class="detail-section" aria-labelledby="codeDetailTableLabel">
|
||||
<h2 class="detail-section-hd" id="codeDetailTableLabel">
|
||||
세부코드 — <?= esc($selectedKind->ck_name) ?> (<?= esc($selectedKind->ck_code) ?>)
|
||||
</h2>
|
||||
<div class="table-toolbar">
|
||||
<div class="total">세부 <strong><?= number_format(count($detailList)) ?></strong> 건</div>
|
||||
<div class="tools">
|
||||
<?php if ($canManageDetails): ?>
|
||||
<a href="<?= base_url('admin/code-details/' . (int) $selectedKind->ck_idx . '/create') ?>" class="btn-portal btn-portal-primary">
|
||||
<i class="fa-solid fa-plus"></i> 세부코드 등록
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="portal-data-table-wrap">
|
||||
<table class="portal-data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>번호</th>
|
||||
<th>코드</th>
|
||||
<th>코드명</th>
|
||||
<th>범위</th>
|
||||
<th>정렬</th>
|
||||
<th>사용여부</th>
|
||||
<th>등록일</th>
|
||||
<?php if ($canManageDetails): ?>
|
||||
<th>작업</th>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($detailList !== []): ?>
|
||||
<?php $dNo = 0; foreach ($detailList as $row): $dNo++; ?>
|
||||
<?php
|
||||
$isPlatform = (($row->cd_source ?? 'platform') === 'platform' && (int) ($row->cd_lg_idx ?? 0) === 0);
|
||||
$scopeLabel = $isPlatform ? '공통' : '지자체';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= (string) $dNo ?></td>
|
||||
<td class="font-mono"><?= esc($row->cd_code) ?></td>
|
||||
<td class="text-left"><?= esc($row->cd_name) ?></td>
|
||||
<td><?= esc($scopeLabel) ?></td>
|
||||
<td><?= (int) ($row->cd_sort ?? 0) ?></td>
|
||||
<td><?= (int) ($row->cd_state ?? 0) === 1 ? '사용' : '미사용' ?></td>
|
||||
<td><?= esc($row->cd_regdate ?? '') ?></td>
|
||||
<?php if ($canManageDetails): ?>
|
||||
<td>
|
||||
<?php if (! empty($rowCanEdit[$row->cd_idx])): ?>
|
||||
<a href="<?= base_url('admin/code-details/edit/' . (int) $row->cd_idx) ?>">수정</a>
|
||||
<?php else: ?>
|
||||
<span style="color:#aaa">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr class="empty-row">
|
||||
<td colspan="<?= $canManageDetails ? '8' : '7' ?>">등록된 세부코드가 없습니다.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<?php elseif ($codeKinds !== []): ?>
|
||||
<p style="font-size:0.75rem;color:#888;margin-top:0.75rem;">위 표에서 코드 종류를 선택하면 세부코드가 표시됩니다.</p>
|
||||
<?php endif; ?>
|
||||
638
app/Views/home/dashboard_gov_portal.php
Normal file
638
app/Views/home/dashboard_gov_portal.php
Normal file
@@ -0,0 +1,638 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* 공공 포털형 UI — 기본 레이아웃 (좌측 MY MENU)
|
||||
*
|
||||
* @var string $lgLabel
|
||||
* @var string $activeVariant
|
||||
*/
|
||||
helper('admin');
|
||||
$govPortalNavPartial = gov_portal_nav_partial_vars(get_defined_vars());
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?= view('home/_dashboard_gov_portal_head') ?>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"/>
|
||||
<?= view('home/_dashboard_gov_portal_map_leaflet_assets') ?>
|
||||
<style>
|
||||
:root {
|
||||
--navy: #1a2b4b;
|
||||
--navy-deep: #002b4e;
|
||||
--blue: #0056b3;
|
||||
--blue-ui: #007bff;
|
||||
--blue-menu: #4a69bd;
|
||||
--blue-light: #eef6ff;
|
||||
--teal: #009688;
|
||||
--bg: #f0f4f8;
|
||||
--card: #fff;
|
||||
--text: #444;
|
||||
--text-dark: #222;
|
||||
--muted: #888;
|
||||
--border: #dde4ec;
|
||||
--font-scale: 1;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html {
|
||||
font-size: calc(14px * var(--font-scale));
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
|
||||
font-size: 0.8125rem; /* ~13px @14px root — 본문 */
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_brand_css.php'; ?>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_topnav_css.php'; ?>
|
||||
.layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
/* 좌측 사이드 */
|
||||
.sidebar {
|
||||
width: 168px;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.my-menu-hd {
|
||||
background: var(--navy);
|
||||
color: #fff;
|
||||
padding: 0.5rem 0.625rem;
|
||||
font-weight: 700;
|
||||
font-size: 0.6875rem; /* 11px — MY MENU 라벨 */
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.my-menu-list { list-style: none; padding: 0.375rem 0.25rem; flex: 1; }
|
||||
.my-menu-list li { margin: 0.1875rem 0.375rem; }
|
||||
.my-menu-list a,
|
||||
.my-menu-list .menu-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.4375rem 0.625rem;
|
||||
margin: 0;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
text-decoration: none;
|
||||
font-size: 0.8125rem; /* 13px */
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.02em;
|
||||
box-sizing: border-box;
|
||||
transition: filter 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.my-menu-list a {
|
||||
color: #fff;
|
||||
background: var(--blue-menu);
|
||||
}
|
||||
.my-menu-list a .menu-ico,
|
||||
.my-menu-list .menu-sub .menu-ico {
|
||||
font-size: 0.625rem;
|
||||
opacity: .9;
|
||||
width: 0.75rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.my-menu-list a:hover { filter: brightness(1.06); border-color: rgba(255, 255, 255, 0.35); }
|
||||
.my-menu-list a.active {
|
||||
background: #3d5a9e;
|
||||
font-weight: 700;
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
box-shadow: 0 1px 3px rgba(26, 43, 75, 0.12);
|
||||
}
|
||||
.my-menu-list a.menu-sub,
|
||||
.my-menu-list .menu-sub {
|
||||
background: var(--blue-light);
|
||||
color: var(--blue);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-color: rgba(0, 86, 179, 0.18);
|
||||
}
|
||||
.sidebar-blocks { margin-top: auto; }
|
||||
.sb-teal {
|
||||
background: var(--teal);
|
||||
color: #fff;
|
||||
padding: 0.75rem 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.sb-teal i { font-size: 1.125rem; margin-bottom: 0.25rem; display: block; }
|
||||
.sb-gray {
|
||||
background: #4a5568;
|
||||
color: #fff;
|
||||
padding: 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.sb-links {
|
||||
padding: 0.625rem;
|
||||
background: #f5f7fa;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
.sb-links a {
|
||||
display: block;
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
padding: 0.1875rem 0;
|
||||
letter-spacing: -0.02em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.sb-links a:hover { color: var(--blue-ui); }
|
||||
|
||||
/* 메인 그리드 */
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 0.875rem 1rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
gap: 0.875rem; /* ~14px */
|
||||
align-items: stretch;
|
||||
}
|
||||
.grid .card-low-stock.stock-tall {
|
||||
grid-row: span 2;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(26,43,75,.06), 0 2px 8px rgba(26,43,75,.04);
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.625rem 0.875rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-weight: 700;
|
||||
font-size: 1rem; /* ~16px 카드 제목 */
|
||||
color: var(--text-dark);
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.card-hd i {
|
||||
color: var(--blue-ui);
|
||||
margin-right: 0.3rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.card-bd { padding: 0.875rem 1rem; }
|
||||
.span-3 { grid-column: span 3; }
|
||||
.span-4 { grid-column: span 4; }
|
||||
.span-5 { grid-column: span 5; }
|
||||
.span-6 { grid-column: span 6; }
|
||||
.span-8 { grid-column: span 8; }
|
||||
.span-12 { grid-column: span 12; }
|
||||
@media (max-width: 1200px) {
|
||||
.span-3, .span-4, .span-5, .span-6, .span-8 { grid-column: span 12; }
|
||||
.grid .card-low-stock.stock-tall { grid-row: span 1; }
|
||||
.sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); }
|
||||
.layout { flex-direction: column; }
|
||||
.my-menu-list { display: flex; flex-wrap: wrap; }
|
||||
}
|
||||
|
||||
.card-welcome {
|
||||
background: #4a5568;
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: 0 2px 6px rgba(74,85,104,.25);
|
||||
}
|
||||
.card-welcome .card-bd { padding: 1rem 0.875rem; }
|
||||
.welcome-hi {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255,255,255,.75);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.welcome-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin: 0.25rem 0 0.375rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.welcome-meta {
|
||||
font-size: 0.6875rem;
|
||||
color: rgba(255,255,255,.7);
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.welcome-btns { display: flex; gap: 0.375rem; margin-top: 0.625rem; }
|
||||
.welcome-btns a {
|
||||
padding: 0.3125rem 0.625rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border: 1px solid rgba(255,255,255,.35);
|
||||
background: rgba(255,255,255,.08);
|
||||
color: #fff;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.welcome-btns a:hover { background: rgba(255,255,255,.18); }
|
||||
|
||||
.notice-item {
|
||||
padding: 0.4375rem 0;
|
||||
border-bottom: 1px dashed #e8edf2;
|
||||
}
|
||||
.notice-item:last-child { border-bottom: none; }
|
||||
.notice-item .notice-title {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
margin-bottom: 0.25rem;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.notice-item .notice-date {
|
||||
display: inline-block;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
color: var(--blue-ui);
|
||||
background: var(--blue-light);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 2px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.375rem;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-grid .num {
|
||||
font-size: 1.5rem; /* ~22px */
|
||||
font-weight: 700;
|
||||
color: #2563eb;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.stat-grid .num.num-text { font-size: 1.125rem; color: #10b981; }
|
||||
.stat-grid .lbl {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--muted);
|
||||
margin-top: 0.1875rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.stat-foot {
|
||||
margin-top: 0.625rem;
|
||||
padding-top: 0.4375rem;
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 0.6875rem;
|
||||
color: var(--muted);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_stock_cards_css.php'; ?>
|
||||
|
||||
.search-teal {
|
||||
background: var(--teal);
|
||||
color: #fff;
|
||||
padding: 0.875rem;
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
min-height: 5.5rem;
|
||||
}
|
||||
.search-teal strong {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.search-teal input {
|
||||
width: 100%;
|
||||
margin-top: 0.4375rem;
|
||||
padding: 0.4375rem 1.75rem 0.4375rem 0.625rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8125rem;
|
||||
font-family: inherit;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.search-wrap { position: relative; }
|
||||
.search-wrap i {
|
||||
position: absolute;
|
||||
right: 0.65rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--muted);
|
||||
}
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_menu_search_css.php'; ?>
|
||||
|
||||
.desk-blue {
|
||||
background: linear-gradient(160deg, #1a4a8a 0%, #007bff 100%);
|
||||
color: #fff;
|
||||
padding: 0.875rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.65;
|
||||
letter-spacing: -0.02em;
|
||||
height: 100%;
|
||||
min-height: 5.5rem;
|
||||
}
|
||||
.desk-blue strong {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
margin-bottom: 0.3rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_map_css.php'; ?>
|
||||
.timeline-side {
|
||||
background: var(--navy-deep);
|
||||
color: #fff;
|
||||
padding: 0.375rem 0.25rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.timeline-side .item {
|
||||
padding: 0.375rem 0.3125rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,.12);
|
||||
}
|
||||
.timeline-side .time {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
.timeline-side .ev-text {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #4fc3f7;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.donut-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.donut {
|
||||
width: 82px;
|
||||
height: 82px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(#3b82f6 0% 52%, #10b981 52% 80%, #f59e0b 80% 100%);
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.donut::after {
|
||||
content: '52%';
|
||||
position: absolute;
|
||||
inset: 24%;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
color: var(--navy);
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.donut-legend { list-style: none; font-size: 0.6875rem; color: var(--muted); line-height: 1.5; }
|
||||
.mini-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
height: 64px;
|
||||
}
|
||||
.mini-bars .col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 2px;
|
||||
font-size: 0.625rem;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.mini-bars .bar {
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, #2563eb, #60a5fa);
|
||||
border-radius: 3px 3px 0 0;
|
||||
min-height: 4px;
|
||||
}
|
||||
.gis-btn {
|
||||
background: var(--blue-ui);
|
||||
color: #fff;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
text-decoration: none;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.flash-ok {
|
||||
grid-column: 1 / -1;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="portal-header">
|
||||
<div class="portal-header-inner">
|
||||
<?= view('home/_dashboard_gov_portal_brand', ['brandHref' => base_url('dashboard/gov-portal')]) ?>
|
||||
<?= view('home/_dashboard_gov_portal_topnav_click', $govPortalNavPartial) ?>
|
||||
<?= view('home/_dashboard_gov_portal_header_utils', ['activeVariant' => $activeVariant, 'portalVariants' => $portalVariants, 'mbName' => $mbName, 'levelName' => $levelName, 'lgLabel' => $lgLabel]) ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<?= view('home/_dashboard_gov_portal_sidebar', $govPortalNavPartial) ?>
|
||||
|
||||
<main class="main">
|
||||
<div class="grid">
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="flash-ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 사용자 정보 (스크린샷: 다크 그레이 프로필 카드) -->
|
||||
<div class="card card-welcome span-3">
|
||||
<div class="card-bd">
|
||||
<p class="welcome-hi">안녕하세요.</p>
|
||||
<p class="welcome-name"><?= esc($mbName) ?>님</p>
|
||||
<p class="welcome-meta">아이디 <?= esc($mbId) ?><br/>최근접속 <?= date('Y.m.d H:i') ?></p>
|
||||
<div class="welcome-btns">
|
||||
<a href="<?= base_url(gov_portal_code_kinds_portal_path('base')) ?>">기본 코드관리</a>
|
||||
<a href="<?= base_url('dashboard/simple') ?>">마이페이지</a>
|
||||
<a href="<?= base_url('logout') ?>">로그아웃</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메시지 -->
|
||||
<div class="card span-4">
|
||||
<div class="card-hd"><span><i class="fa-regular fa-envelope"></i> 메시지</span></div>
|
||||
<div class="card-bd">
|
||||
<?php foreach ($notices as $n): ?>
|
||||
<div class="notice-item">
|
||||
<span class="notice-title"><?= esc($n['title']) ?></span>
|
||||
<span class="notice-date"><?= esc($n['date']) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 핵심 지표 (메인 요약과 동일) -->
|
||||
<div class="card span-5">
|
||||
<div class="card-hd"><span><i class="fa-solid fa-warehouse"></i> 업무 현황 요약</span></div>
|
||||
<div class="card-bd">
|
||||
<div class="stat-grid">
|
||||
<div>
|
||||
<div class="num num-text">양호</div>
|
||||
<div class="lbl">봉투 재고</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="num">12</div>
|
||||
<div class="lbl">미처리 구매신청</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="num">4</div>
|
||||
<div class="lbl">승인 대기</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-foot">
|
||||
<i class="fa-solid fa-location-dot"></i> <?= esc($lgLabel) ?> · 기준일 <?= date('Y.m.d') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 지도/타임라인 영역 -->
|
||||
<div class="card span-8" style="padding:0;">
|
||||
<div class="card-hd">
|
||||
<span><i class="fa-solid fa-map"></i> 판매·수불 최근 동향</span>
|
||||
<a href="<?= base_url('bag/flow') ?>" class="gis-btn">수불 통합 조회 ></a>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 140px;">
|
||||
<?= view('home/_dashboard_gov_portal_map_panel', [
|
||||
'mapId' => 'govPortalMainMap',
|
||||
'mapHeight' => '200px',
|
||||
'lgLabel' => $lgLabel,
|
||||
'govMapPanel' => $govMapPanel,
|
||||
]) ?>
|
||||
<div class="timeline-side">
|
||||
<?php foreach ($timeline as $ev): ?>
|
||||
<div class="item">
|
||||
<span class="time"><?= esc($ev['time']) ?></span>
|
||||
<span class="ev-text"><?= esc($ev['text']) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메뉴 검색 -->
|
||||
<div class="span-4">
|
||||
<?= view('home/_dashboard_gov_portal_menu_search', [
|
||||
'variant' => 'teal',
|
||||
'inputId' => 'menuSearch',
|
||||
'menuSearchOptions' => $menuSearchOptions,
|
||||
]) ?>
|
||||
</div>
|
||||
|
||||
<!-- 서비스 데스크 -->
|
||||
<div class="span-4">
|
||||
<div class="desk-blue">
|
||||
<strong><i class="fa-solid fa-headset"></i> 서비스 데스크</strong>
|
||||
담당: 시스템 운영팀<br/>
|
||||
문의: help@wxn.local (목업)<br/>
|
||||
평일 09:00 ~ 18:00
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 재고 경보 단계 -->
|
||||
<div class="card span-4">
|
||||
<div class="card-hd"><span><i class="fa-solid fa-triangle-exclamation"></i> 재고 경보</span></div>
|
||||
<div class="card-bd">
|
||||
<?= view('home/_dashboard_gov_portal_stock_alert_levels', ['stockAlerts' => $stockAlerts]) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 부족 재고 — 2행 span으로 재고 구성 오른쪽 빈칸(4열)까지 세로 채움 -->
|
||||
<div class="card card-low-stock stock-tall span-4">
|
||||
<div class="card-hd"><span><i class="fa-solid fa-box-open"></i> 부족 재고</span></div>
|
||||
<div class="card-bd">
|
||||
<div class="low-stock-grid">
|
||||
<?php foreach ($lowStock as $item): ?>
|
||||
<div class="bar-row">
|
||||
<div class="meta"><span><?= esc($item['name']) ?></span><span><?= esc((string) $item['percent']) ?>%</span></div>
|
||||
<div class="bar-track"><div class="bar-fill" style="width:<?= (int) $item['percent'] ?>%"></div></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 7일 추이 -->
|
||||
<div class="card span-4">
|
||||
<div class="card-hd"><span><i class="fa-solid fa-chart-column"></i> 최근 7일 신청</span></div>
|
||||
<div class="card-bd">
|
||||
<?php $maxReq = max($weeklyRequests); ?>
|
||||
<div class="mini-bars">
|
||||
<?php foreach ($weeklyRequests as $idx => $v): ?>
|
||||
<?php $h = (int) round(($v / $maxReq) * 100); ?>
|
||||
<div class="col">
|
||||
<span><?= esc((string) $v) ?></span>
|
||||
<div class="bar" style="height:<?= $h ?>%"></div>
|
||||
<span>D<?= 6 - $idx ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 재고 구성 -->
|
||||
<div class="card span-4">
|
||||
<div class="card-hd"><span><i class="fa-solid fa-chart-pie"></i> 재고 구성</span></div>
|
||||
<div class="card-bd">
|
||||
<div class="donut-wrap">
|
||||
<div class="donut" aria-hidden="true"></div>
|
||||
<ul class="donut-legend">
|
||||
<?php foreach ($stockMix as $item): ?>
|
||||
<li>
|
||||
<span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:<?= esc($item['color'], 'attr') ?>;vertical-align:middle;margin-right:3px;"></span>
|
||||
<?= esc($item['name']) ?> <?= esc((string) $item['value']) ?>%
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<?= view('home/_dashboard_gov_portal_nav_script_base', $govPortalNavPartial) ?>
|
||||
<?= view('home/_dashboard_gov_portal_font_zoom_script') ?>
|
||||
</body>
|
||||
</html>
|
||||
143
app/Views/home/dashboard_gov_portal_code_kinds.php
Normal file
143
app/Views/home/dashboard_gov_portal_code_kinds.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* 공공 포털형 — 기본 코드관리 (NDMS 스타일 본문 + gov-portal 상단·사이드 메뉴)
|
||||
*/
|
||||
helper('admin');
|
||||
$govPortalNavPartial = gov_portal_nav_partial_vars(get_defined_vars());
|
||||
$activeVariant = $activeVariant ?? 'base';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<?= view('home/_dashboard_gov_portal_head') ?>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"/>
|
||||
<style>
|
||||
:root {
|
||||
--navy: #1a2b4b;
|
||||
--navy-deep: #002b4e;
|
||||
--blue: #0056b3;
|
||||
--blue-ui: #007bff;
|
||||
--blue-menu: #4a69bd;
|
||||
--blue-light: #eef6ff;
|
||||
--teal: #009688;
|
||||
--bg: #f0f4f8;
|
||||
--text: #444;
|
||||
--border: #dde4ec;
|
||||
--font-scale: 1;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html {
|
||||
font-size: calc(14px * var(--font-scale));
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_brand_css.php'; ?>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_topnav_css.php'; ?>
|
||||
<?php include __DIR__ . '/_dashboard_gov_portal_workpage_css.php'; ?>
|
||||
.layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.sidebar {
|
||||
width: 168px;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.my-menu-hd {
|
||||
background: var(--navy);
|
||||
color: #fff;
|
||||
padding: 0.5rem 0.625rem;
|
||||
font-weight: 700;
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.my-menu-list { list-style: none; padding: 0.375rem 0.25rem; flex: 1; }
|
||||
.my-menu-list li { margin: 0.1875rem 0.375rem; }
|
||||
.my-menu-list a,
|
||||
.my-menu-list .menu-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.4375rem 0.625rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
text-decoration: none;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.my-menu-list a {
|
||||
color: #fff;
|
||||
background: var(--blue-menu);
|
||||
}
|
||||
.my-menu-list a .menu-ico { font-size: 0.625rem; width: 0.75rem; text-align: center; }
|
||||
.my-menu-list a.active {
|
||||
background: #3d5a9e;
|
||||
font-weight: 700;
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.sidebar-blocks { padding: 0.5rem; font-size: 0.6875rem; }
|
||||
.sb-teal {
|
||||
background: var(--teal);
|
||||
color: #fff;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.35rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.sb-gray {
|
||||
background: #6b7280;
|
||||
color: #fff;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.35rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.sb-links a { display: block; color: #3366ff; margin-top: 0.25rem; font-size: 0.6875rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="portal-header">
|
||||
<div class="portal-header-inner">
|
||||
<?= view('home/_dashboard_gov_portal_brand', ['brandHref' => base_url('dashboard/gov-portal')]) ?>
|
||||
<?= view('home/_dashboard_gov_portal_topnav_click', $govPortalNavPartial) ?>
|
||||
<?= view('home/_dashboard_gov_portal_header_utils', [
|
||||
'activeVariant' => $activeVariant,
|
||||
'portalVariants' => $portalVariants,
|
||||
'mbName' => $mbName,
|
||||
'levelName' => $levelName,
|
||||
'lgLabel' => $lgLabel,
|
||||
]) ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<?= view('home/_dashboard_gov_portal_sidebar', $govPortalNavPartial) ?>
|
||||
<main class="main work-main">
|
||||
<?= view('home/_gov_portal_code_kinds_body', get_defined_vars()) ?>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<?= view('home/_dashboard_gov_portal_nav_script_base', $govPortalNavPartial) ?>
|
||||
<?= view('home/_dashboard_gov_portal_font_zoom_script') ?>
|
||||
</body>
|
||||
</html>
|
||||
8
app/Views/home/dashboard_gov_portal_strip.php
Normal file
8
app/Views/home/dashboard_gov_portal_strip.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @deprecated 컨트롤러가 _dashboard_gov_portal_strip_layout 을 직접 렌더합니다.
|
||||
*/
|
||||
echo view('home/_dashboard_gov_portal_strip_layout', array_merge(get_defined_vars(), [
|
||||
'stripInnerView' => 'home/_dashboard_gov_portal_strip_home_inner',
|
||||
]));
|
||||
10
app/Views/home/dashboard_gov_portal_strip_code_kinds.php
Normal file
10
app/Views/home/dashboard_gov_portal_strip_code_kinds.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @deprecated 컨트롤러가 _dashboard_gov_portal_strip_layout 을 직접 렌더합니다.
|
||||
*/
|
||||
echo view('home/_dashboard_gov_portal_strip_layout', array_merge(get_defined_vars(), [
|
||||
'stripInnerView' => 'home/_gov_portal_code_kinds_body',
|
||||
'stripIncludeWorkCss' => true,
|
||||
'stripShowProfileLinks' => true,
|
||||
]));
|
||||
Reference in New Issue
Block a user