Compare commits
6 Commits
142ed1110d
...
249053412c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
249053412c | ||
|
|
de15cc9e47 | ||
|
|
a5d995588b | ||
|
|
ace0ce6296 | ||
|
|
b3518f6777 | ||
|
|
9b3f5e4701 |
@@ -47,7 +47,7 @@ class Manual extends BaseConfig
|
||||
*/
|
||||
public array $screenHelp = [
|
||||
'bag/order/phone' => 'sales#전화 주문 접수',
|
||||
'bag/order/lot-seed' => 'order#LOT-No 디스켓 불출',
|
||||
'bag/order/lot-seed' => 'order#발주파일 생성',
|
||||
'bag/order' => 'order#발주 등록',
|
||||
'bag/password-change' => 'account#비밀번호 변경',
|
||||
'bag/bag-orders' => 'order#발주 현황',
|
||||
@@ -73,6 +73,8 @@ class Manual extends BaseConfig
|
||||
'bag/reports/hometax-export' => 'reports#그 밖의 현황',
|
||||
'bag/reports' => 'reports#일계표',
|
||||
'bag/analytics' => 'reports#그 밖의 현황',
|
||||
'bag/designated-shops/status' => 'basic#지정 판매소 신규/취소 현황',
|
||||
'bag/designated-shops/district-new-cancel' => 'basic#지정 판매소 신규/취소 현황',
|
||||
'bag/designated-shops' => 'basic#지정판매소 관리',
|
||||
'bag/bag-prices' => 'basic#단가 관리',
|
||||
'bag/prices' => 'basic#단가 관리',
|
||||
|
||||
@@ -83,6 +83,7 @@ $routes->get('bag/order/phone/receipt/(:num)', 'Bag::phoneOrderReceipt/$1');
|
||||
$routes->get('bag/order/change', 'Bag::orderChange');
|
||||
$routes->get('bag/order/revise/(:num)', 'Bag::orderRevise/$1');
|
||||
$routes->get('bag/order/lot-seed', 'Bag::orderLotSeed');
|
||||
$routes->get('bag/order/lot-seed/preview', 'Bag::orderLotSeedPreview');
|
||||
$routes->post('bag/order/lot-seed/generate', 'Bag::orderLotSeedGenerate');
|
||||
$routes->post('bag/order/store', 'Bag::orderStore');
|
||||
$routes->post('bag/order/cancel/(:num)', 'Bag::orderCancel/$1');
|
||||
|
||||
@@ -256,8 +256,31 @@ class Menu extends BaseController
|
||||
return redirect()->to(base_url('admin/select-local-government'))
|
||||
->with('error', '지자체를 선택하세요.');
|
||||
}
|
||||
$ids = $this->request->getPost('mm_idx');
|
||||
$postMtIdx = (int) $this->request->getPost('mt_idx');
|
||||
|
||||
// 순서 + 상위(부모) 변경 구조가 함께 전달되면 우선 처리 (하위 메뉴의 상위 이동 지원)
|
||||
$treeJson = (string) $this->request->getPost('menu_tree');
|
||||
$structure = $treeJson !== '' ? json_decode($treeJson, true) : null;
|
||||
|
||||
if (is_array($structure) && $structure !== []) {
|
||||
$orderedIds = array_values(array_filter(array_map(
|
||||
static fn ($it): int => (int) ($it['idx'] ?? 0),
|
||||
$structure
|
||||
), static fn (int $v): bool => $v > 0));
|
||||
if (empty($orderedIds)) {
|
||||
return $this->menusRedirect($postMtIdx)->with('error', '순서를 적용할 메뉴가 없습니다.');
|
||||
}
|
||||
$firstRow = $this->menuModel->find($orderedIds[0]);
|
||||
$this->menuModel->setStructure($structure, $lgIdx);
|
||||
if ($firstRow && (int) $firstRow->lg_idx === $lgIdx) {
|
||||
$this->menuModel->pruneInventoryManagementMenus((int) $firstRow->mt_idx, $lgIdx);
|
||||
$this->menuModel->syncTypeToAllLgs((int) $firstRow->mt_idx, $lgIdx);
|
||||
}
|
||||
$mtIdx = $firstRow ? (int) $firstRow->mt_idx : $postMtIdx;
|
||||
return $this->menusRedirect($mtIdx)->with('success', '순서가 적용되었습니다.');
|
||||
}
|
||||
|
||||
$ids = $this->request->getPost('mm_idx');
|
||||
if (! is_array($ids) || empty($ids)) {
|
||||
return $this->menusRedirect($postMtIdx)->with('error', '순서를 적용할 메뉴가 없습니다.');
|
||||
}
|
||||
|
||||
@@ -3953,7 +3953,7 @@ SQL);
|
||||
}
|
||||
|
||||
/**
|
||||
* LOT-No 디스켓 불출: 발주 건을 선택해 암호화 seed 파일 생성/다운로드.
|
||||
* 발주파일 생성(구 'LOT-No 디스켓 불출'): 발주 건을 선택해 암호화 seed 파일 생성/다운로드.
|
||||
*/
|
||||
public function orderLotSeed(): string|RedirectResponse
|
||||
{
|
||||
@@ -4031,7 +4031,7 @@ SQL);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('LOT-No 디스켓 불출', 'bag/order_lot_seed', [
|
||||
return $this->render('발주파일 생성', 'bag/order_lot_seed', [
|
||||
'orders' => $orders,
|
||||
'pager' => $pager,
|
||||
'startMonth' => $startMonth,
|
||||
@@ -4044,17 +4044,16 @@ SQL);
|
||||
]);
|
||||
}
|
||||
|
||||
public function orderLotSeedGenerate(): RedirectResponse|ResponseInterface
|
||||
/**
|
||||
* 발주파일(seed) 생성·미리보기가 공용으로 쓰는 데이터 구성.
|
||||
* 발주 메타(order_data)와 품목(items, 팩/박스 환산 포함), 무결성 해시를 만든다.
|
||||
*
|
||||
* @return array{ok: bool, error?: string, order?: object, order_data?: array<string,mixed>, items?: array<int,array<string,mixed>>, lot_no?: string, uuid?: string, version?: int, order_hash?: string}
|
||||
*/
|
||||
private function buildLotSeedData(int $lgIdx, int $boIdx): array
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
if (! $lgIdx) {
|
||||
return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$boIdx = (int) ($this->request->getPost('bo_idx') ?? 0);
|
||||
if ($boIdx <= 0) {
|
||||
return redirect()->back()->with('error', '발주 건을 선택해 주세요.');
|
||||
return ['ok' => false, 'error' => '발주 건을 선택해 주세요.'];
|
||||
}
|
||||
|
||||
$order = model(BagOrderModel::class)
|
||||
@@ -4062,14 +4061,14 @@ SQL);
|
||||
->where('bo_idx', $boIdx)
|
||||
->first();
|
||||
if (! $order) {
|
||||
return redirect()->back()->with('error', '발주 정보를 찾을 수 없습니다.');
|
||||
return ['ok' => false, 'error' => '발주 정보를 찾을 수 없습니다.'];
|
||||
}
|
||||
|
||||
$lotNo = trim((string) ($order->bo_lot_no ?? ''));
|
||||
$uuid = trim((string) ($order->bo_uuid ?? ''));
|
||||
$version = max(1, (int) ($order->bo_version ?? 1));
|
||||
if ($lotNo === '' || $uuid === '') {
|
||||
return redirect()->back()->with('error', '발주의 LOT/UUID 정보가 없어 seed 파일을 생성할 수 없습니다.');
|
||||
return ['ok' => false, 'error' => '발주의 LOT/UUID 정보가 없어 발주파일을 생성할 수 없습니다.'];
|
||||
}
|
||||
|
||||
$items = model(BagOrderItemModel::class)
|
||||
@@ -4077,7 +4076,7 @@ SQL);
|
||||
->orderBy('boi_idx', 'ASC')
|
||||
->findAll();
|
||||
if ($items === []) {
|
||||
return redirect()->back()->with('error', '발주 품목이 없어 seed 파일을 생성할 수 없습니다.');
|
||||
return ['ok' => false, 'error' => '발주 품목이 없어 발주파일을 생성할 수 없습니다.'];
|
||||
}
|
||||
|
||||
$unitRows = model(PackagingUnitModel::class)
|
||||
@@ -4142,10 +4141,100 @@ SQL);
|
||||
$orderHash = hash('sha256', $payloadJson);
|
||||
}
|
||||
|
||||
$seedPath = $this->generateLotSeedFile($uuid, $version, $lotNo, $orderData, $hashItems, $orderHash);
|
||||
return [
|
||||
'ok' => true,
|
||||
'order' => $order,
|
||||
'order_data' => $orderData,
|
||||
'items' => $hashItems,
|
||||
'lot_no' => $lotNo,
|
||||
'uuid' => $uuid,
|
||||
'version' => $version,
|
||||
'order_hash' => $orderHash,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주파일 생성 전 '내용 확인'(미리보기): 암호화 전 평문 내용을 JSON 으로 반환.
|
||||
*/
|
||||
public function orderLotSeedPreview(): ResponseInterface
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
if (! $lgIdx) {
|
||||
return $this->response->setJSON(['ok' => false, 'error' => '지자체를 선택해 주세요.']);
|
||||
}
|
||||
|
||||
$boIdx = (int) ($this->request->getGet('bo_idx') ?? 0);
|
||||
$data = $this->buildLotSeedData($lgIdx, $boIdx);
|
||||
if (! ($data['ok'] ?? false)) {
|
||||
return $this->response->setJSON(['ok' => false, 'error' => (string) ($data['error'] ?? '내용을 확인할 수 없습니다.')]);
|
||||
}
|
||||
|
||||
$order = $data['order'];
|
||||
$companyIdx = (int) ($order->bo_company_idx ?? 0);
|
||||
$companyName = '';
|
||||
if ($companyIdx > 0) {
|
||||
$company = model(CompanyModel::class)
|
||||
->where('cp_lg_idx', $lgIdx)
|
||||
->where('cp_idx', $companyIdx)
|
||||
->first();
|
||||
$companyName = (string) ($company->cp_name ?? '');
|
||||
}
|
||||
|
||||
$items = array_map(static fn (array $it): array => [
|
||||
'bag_code' => (string) $it['boi_bag_code'],
|
||||
'bag_name' => (string) $it['boi_bag_name'],
|
||||
'unit_price' => (float) $it['boi_unit_price'],
|
||||
'qty_box' => (int) $it['boi_qty_box'],
|
||||
'qty_pack' => (int) $it['boi_qty_pack'],
|
||||
'qty_sheet' => (int) $it['boi_qty_sheet'],
|
||||
'amount' => (float) $it['boi_amount'],
|
||||
], $data['items']);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => true,
|
||||
'bo_idx' => $boIdx,
|
||||
'lot_no' => $data['lot_no'],
|
||||
'uuid' => $data['uuid'],
|
||||
'version' => $data['version'],
|
||||
'order_date' => (string) ($order->bo_order_date ?? ''),
|
||||
'company' => $companyName,
|
||||
'order_hash' => $data['order_hash'],
|
||||
'items' => $items,
|
||||
'totals' => [
|
||||
'line_count' => count($items),
|
||||
'qty_box' => array_sum(array_map(static fn ($i) => (int) $i['qty_box'], $items)),
|
||||
'qty_sheet' => array_sum(array_map(static fn ($i) => (int) $i['qty_sheet'], $items)),
|
||||
'amount' => array_sum(array_map(static fn ($i) => (float) $i['amount'], $items)),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function orderLotSeedGenerate(): RedirectResponse|ResponseInterface
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
if (! $lgIdx) {
|
||||
return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$boIdx = (int) ($this->request->getPost('bo_idx') ?? 0);
|
||||
$data = $this->buildLotSeedData($lgIdx, $boIdx);
|
||||
if (! ($data['ok'] ?? false)) {
|
||||
return redirect()->back()->with('error', (string) ($data['error'] ?? '발주파일을 생성할 수 없습니다.'));
|
||||
}
|
||||
|
||||
$seedPath = $this->generateLotSeedFile(
|
||||
$data['uuid'],
|
||||
$data['version'],
|
||||
$data['lot_no'],
|
||||
$data['order_data'],
|
||||
$data['items'],
|
||||
$data['order_hash']
|
||||
);
|
||||
$seedBinary = @file_get_contents($seedPath);
|
||||
if (! is_string($seedBinary) || $seedBinary === '') {
|
||||
return redirect()->back()->with('error', 'seed 파일 생성에는 성공했으나 파일을 읽을 수 없습니다.');
|
||||
return redirect()->back()->with('error', '발주파일 생성에는 성공했으나 파일을 읽을 수 없습니다.');
|
||||
}
|
||||
|
||||
return $this->response
|
||||
|
||||
@@ -67,14 +67,17 @@
|
||||
|
||||
---
|
||||
|
||||
## LOT-No 디스켓 불출 · *발주 입고 관리 › LOT-No 디스켓 불출*
|
||||
## 발주파일 생성 · *발주 입고 관리 › 발주파일 생성*
|
||||
|
||||
발주별 **LOT 번호 시드(seed) 데이터를 파일로 생성**하는 화면입니다. (바코드·LOT 부여에 쓰는 디스켓/파일 형태 데이터)
|
||||
선택한 발주 건의 내용을 **봉투 생산기(바코드 인쇄) 전달용 파일**로 만드는 화면입니다. (예전 이름: *LOT-No 디스켓 불출*) 생성되는 파일은 발주 정보와 품목이 담긴 **암호화된 발주파일**(`.seed.json`)이며, 봉투 생산 장비가 이 파일을 읽어 그 발주의 바코드를 인쇄합니다.
|
||||
|
||||
**LOT-No란?** 발주를 등록할 때 그 건에 자동 부여되는 **6자리 배치(묶음) 번호**입니다. 이후 그 발주로 인쇄되는 모든 봉투 바코드의 앞자리(예: `ZLCH2M-000006-…`)가 되어, 어느 발주에서 나온 봉투인지 구분하는 기준이 됩니다.
|
||||
|
||||
**필터**: 시작월 · 종료월 · LOT-No · 제작업체.
|
||||
**순서**
|
||||
1. 기간·조건으로 발주 목록을 조회합니다.
|
||||
2. 해당 발주 행의 **`seed 생성`** 을 눌러 그 발주의 LOT 시드 파일을 만듭니다.
|
||||
2. 대상 발주 행의 **`내용 확인`** 을 누르면, 파일에 담길 **발주 정보와 품목 목록(봉투코드·수량·금액 등)** 을 팝업으로 미리 볼 수 있습니다.
|
||||
3. 내용을 확인한 뒤 **`발주파일 생성(다운로드)`** 을 누르면 암호화된 발주파일이 내려받아집니다.
|
||||
|
||||
**표 컬럼**: 발주일자 · LOT-No · 품명 · 수량 · 제작업체 · (생성 버튼).
|
||||
> 정상·취소 발주가 함께 조회되며, 시드 생성은 발주 단위로 동작합니다.
|
||||
**표 컬럼**: 발주일자 · LOT-No · 품명 · 수량 · 제작업체 · (내용 확인 버튼).
|
||||
> 정상·취소 발주가 함께 조회되며, 파일 생성은 발주 단위로 동작합니다. 생성 전 **`내용 확인`** 단계에서 반드시 품목·수량을 검토하세요.
|
||||
|
||||
@@ -19,11 +19,44 @@
|
||||
|
||||
**함께 제공되는 화면**
|
||||
- **지정판매소 바코드 발행**: 판매소를 골라 봉투 바코드 라벨을 인쇄용으로 출력합니다.
|
||||
- **구별 신규/취소 명단**: 기간 내 새로 등록되거나 취소된 판매소 명단을 조회·엑셀저장합니다.
|
||||
- **지정 판매소 신규/취소 현황**: 한 해 동안 군·구별로 몇 곳이 새로 생기고 없어졌는지 **건수**를 집계하는 보고서입니다. (아래 별도 설명)
|
||||
- **지정판매소 조회(브라우즈)**: 등록된 판매소를 검색·열람만 하는 보기 전용 화면입니다.
|
||||
|
||||
---
|
||||
|
||||
## 지정 판매소 신규/취소 현황 · *기본정보관리 › 지정 판매소 신규/취소 현황*
|
||||
|
||||
선택한 **한 해 동안** 지정판매소가 **몇 곳 새로 생기고(지정)**, **몇 곳 없어졌는지(취소)**, 그래서 **연말에 몇 곳이 남았는지**를 **군·구별 건수**로 집계해 보여주는 현황판입니다. 개별 가게 명단이 아니라 **숫자 집계표**입니다.
|
||||
|
||||
**핵심 개념 — 4개의 기둥 컬럼**
|
||||
- **종전(전년도말)**: **전년 12월 31일** 기준 정상 운영 중이던 판매소 수 → *작년 말 잔고*.
|
||||
- **지정(당해년)**: 조회년도 중 **새로 지정**된(신규) 판매소 수.
|
||||
- **취소(당해년)**: 조회년도 중 **취소**(폐업·해지)된 판매소 수.
|
||||
- **현행(금년도말)**: **조회년도 12월 31일** 기준 정상 운영 중인 판매소 수 → *올해 말 잔고*.
|
||||
|
||||
> 관계식: `현행 ≈ 종전 + 지정 − 취소`.
|
||||
> 예) 종전 120 · 지정 15 · 취소 8 → 현행 127. “올해 15곳 새로 지정하고 8곳이 취소되어 순증 7곳, 연말 기준 127곳이 운영 중”이라는 뜻입니다.
|
||||
|
||||
**그 밖의 계산 컬럼**
|
||||
- **구코드**: 판매소에 저장된 구·군 코드 값.
|
||||
- **증감(현행−종전)**: 작년 말 대비 올해 말 **순증감 곳 수**.
|
||||
- **지정−취소**: 그 해 신규에서 취소를 뺀 값(당해 순증).
|
||||
- **현행비중(%)**: 전체 현행 합계에서 그 군·구가 차지하는 비율.
|
||||
- **전년대비 증감률(%)**: `((현행−종전) ÷ 종전) × 100` (종전이 0이면 표시 안 함).
|
||||
- 맨 아래 **합계** 행: 전 군·구 총계.
|
||||
|
||||
**함께 표시되는 것**
|
||||
- **동별 현행 요약**: 선택 군·구 안에서 동(구역)별 현행 수를 칩(badge)과 표로 보여줍니다.
|
||||
- **연도별 요약(참고, 접이식)**: 활성/비활성 합계와 연도별 신규등록·취소 건수.
|
||||
|
||||
**사용법**: 상단에서 **조회년도**를 고르고 **[조회]**. 군·구는 로그인한 지자체 기준으로 자동 고정됩니다. 우측 상단 **엑셀저장 · 인쇄**로 내보낼 수 있습니다. (각 컬럼 제목 옆 **?** 배지에 마우스를 올리면 설명이 나옵니다.)
|
||||
|
||||
> **비슷한 이름과 혼동 주의**
|
||||
> - **지정 판매소 신규/취소 현황**(이 화면): 군·구별 신규/취소 **건수 집계·통계**.
|
||||
> - **지정판매소 관리 / 조회**: **개별 가게**를 등록·검색·열람하는 화면.
|
||||
|
||||
---
|
||||
|
||||
## 단가 관리 · *기본정보관리 › 단가 관리*
|
||||
|
||||
봉투 **가격**을 기간별로 관리합니다.
|
||||
|
||||
@@ -28,13 +28,14 @@ class FeedbackModel extends Model
|
||||
];
|
||||
|
||||
/** 허용 상태 값 */
|
||||
public const STATUSES = ['new', 'in_progress', 'done', 'hold'];
|
||||
public const STATUSES = ['new', 'in_progress', 'answered', 'done', 'hold'];
|
||||
|
||||
public static function statusLabel(string $s): string
|
||||
{
|
||||
return [
|
||||
'new' => '접수',
|
||||
'in_progress' => '처리중',
|
||||
'answered' => '답변 완료',
|
||||
'done' => '완료',
|
||||
'hold' => '보류',
|
||||
][$s] ?? $s;
|
||||
|
||||
@@ -82,6 +82,60 @@ class MenuModel extends Model
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 순서 + 상위(부모)·깊이 변경을 함께 반영한다.
|
||||
* 하위 메뉴를 다른 상위 메뉴로 옮기는 드래그를 지원하기 위한 용도.
|
||||
*
|
||||
* @param array<int,array{idx:int,pidx:int,dep:int}> $items 화면 표시 순서대로 정렬된 구조 배열
|
||||
*/
|
||||
public function setStructure(array $items, int $lgIdx): void
|
||||
{
|
||||
$affectedTypes = [];
|
||||
$i = 0;
|
||||
foreach ($items as $it) {
|
||||
$mmIdx = (int) ($it['idx'] ?? 0);
|
||||
if ($mmIdx <= 0) {
|
||||
continue;
|
||||
}
|
||||
$row = $this->find($mmIdx);
|
||||
if (! $row || (int) $row->lg_idx !== $lgIdx) {
|
||||
continue;
|
||||
}
|
||||
$this->update($mmIdx, [
|
||||
'mm_num' => $i,
|
||||
'mm_pidx' => max(0, (int) ($it['pidx'] ?? 0)),
|
||||
'mm_dep' => max(0, (int) ($it['dep'] ?? 0)),
|
||||
]);
|
||||
$affectedTypes[(int) $row->mt_idx] = true;
|
||||
$i++;
|
||||
}
|
||||
// 상위가 바뀌었을 수 있으므로 종류별로 mm_cnode(자식 수)를 재계산
|
||||
foreach (array_keys($affectedTypes) as $mtIdx) {
|
||||
$this->recountChildNodes((int) $mtIdx, $lgIdx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 메뉴 종류·지자체의 각 메뉴에 대해 mm_cnode(직속 자식 수)를 실제 값으로 재계산한다.
|
||||
*/
|
||||
private function recountChildNodes(int $mtIdx, int $lgIdx): void
|
||||
{
|
||||
$rows = $this->where('mt_idx', $mtIdx)->where('lg_idx', $lgIdx)->findAll();
|
||||
$childCount = [];
|
||||
foreach ($rows as $r) {
|
||||
$pid = (int) $r->mm_pidx;
|
||||
if ($pid > 0) {
|
||||
$childCount[$pid] = ($childCount[$pid] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
foreach ($rows as $r) {
|
||||
$want = (int) ($childCount[(int) $r->mm_idx] ?? 0);
|
||||
if ((int) $r->mm_cnode !== $want) {
|
||||
$this->update((int) $r->mm_idx, ['mm_cnode' => $want]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 추가 시 같은 레벨에서 mm_num 결정 (동일 지자체·메뉴종류·부모·깊이 기준)
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@ $badge = static function (string $s): string {
|
||||
$map = [
|
||||
'new' => 'bg-blue-50 text-blue-700',
|
||||
'in_progress' => 'bg-amber-50 text-amber-700',
|
||||
'answered' => 'bg-teal-50 text-teal-700',
|
||||
'done' => 'bg-emerald-50 text-emerald-700',
|
||||
'hold' => 'bg-gray-100 text-gray-500',
|
||||
];
|
||||
|
||||
@@ -12,6 +12,7 @@ $badge = static function (string $s): string {
|
||||
$map = [
|
||||
'new' => 'bg-blue-50 text-blue-700',
|
||||
'in_progress' => 'bg-amber-50 text-amber-700',
|
||||
'answered' => 'bg-teal-50 text-teal-700',
|
||||
'done' => 'bg-emerald-50 text-emerald-700',
|
||||
'hold' => 'bg-gray-100 text-gray-500',
|
||||
];
|
||||
|
||||
@@ -72,6 +72,7 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
||||
<form id="menu-move-form" method="post" action="<?= base_url('admin/menus/move') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="mt_idx" value="<?= $mtIdx ?>"/>
|
||||
<input type="hidden" name="menu_tree" id="menu-tree-payload" value=""/>
|
||||
<table class="data-table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -92,7 +93,7 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
||||
?>
|
||||
<tr class="menu-row" data-mm-idx="<?= (int) $row->mm_idx ?>" data-mm-pidx="<?= (int) $row->mm_pidx ?>" data-mm-dep="<?= (int) $row->mm_dep ?>">
|
||||
<td class="text-center align-middle">
|
||||
<span class="menu-drag-handle cursor-move text-gray-400 select-none" title="드래그해서 순서를 변경하세요">↕</span>
|
||||
<span class="menu-drag-handle cursor-move text-gray-400 select-none" title="드래그해서 순서 변경 · 하위 메뉴는 다른 상위 메뉴 아래로 이동 가능">↕</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="hidden" name="mm_idx[]" value="<?= (int) $row->mm_idx ?>"/>
|
||||
@@ -444,11 +445,43 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
||||
}
|
||||
};
|
||||
|
||||
// ── 드래그 중 화면 상/하단 가장자리 근처에서 자동 스크롤 ──────────────
|
||||
// 스크롤되는 컨테이너(.main-content-area)가 있으면 그걸, 없으면 window를 스크롤.
|
||||
let autoScrollRaf = null;
|
||||
const getScrollContainer = function() {
|
||||
const main = tbody.closest('.main-content-area');
|
||||
if (main && main.scrollHeight > main.clientHeight + 2) return main;
|
||||
return null; // null이면 window 스크롤
|
||||
};
|
||||
const autoScrollTick = function() {
|
||||
if (!draggingActive) { autoScrollRaf = null; return; }
|
||||
const EDGE = 70; // 가장자리 감지 영역(px)
|
||||
const STEP_MAX = 20; // 프레임당 최대 스크롤(px)
|
||||
const vh = window.innerHeight;
|
||||
let dy = 0;
|
||||
if (lastClientY < EDGE) {
|
||||
dy = -Math.ceil((EDGE - lastClientY) / EDGE * STEP_MAX);
|
||||
} else if (lastClientY > vh - EDGE) {
|
||||
dy = Math.ceil((lastClientY - (vh - EDGE)) / EDGE * STEP_MAX);
|
||||
}
|
||||
if (dy !== 0) {
|
||||
const sc = getScrollContainer();
|
||||
if (sc) { sc.scrollTop += dy; } else { window.scrollBy(0, dy); }
|
||||
// 스크롤로 행 위치가 바뀌었으니 placeholder 위치 재계산
|
||||
if (!rafId) rafId = window.requestAnimationFrame(updatePlaceholderPosition);
|
||||
}
|
||||
autoScrollRaf = window.requestAnimationFrame(autoScrollTick);
|
||||
};
|
||||
|
||||
const clearDragState = function() {
|
||||
if (rafId) {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
rafId = null;
|
||||
}
|
||||
if (autoScrollRaf) {
|
||||
window.cancelAnimationFrame(autoScrollRaf);
|
||||
autoScrollRaf = null;
|
||||
}
|
||||
document.body.classList.remove('menu-row-dragging');
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
@@ -463,41 +496,76 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
||||
draggingActive = false;
|
||||
};
|
||||
|
||||
// 드래그된 행(하위 메뉴)의 새 상위 메뉴를 결정한다.
|
||||
// 평면 목록에서 바로 위쪽으로 올라가며 dep이 (자신-1)인 가장 가까운 행을 상위로 삼는다.
|
||||
const resolveNewParentIdx = function(row, draggedDep) {
|
||||
var prev = row.previousElementSibling;
|
||||
while (prev) {
|
||||
if (prev.classList.contains('menu-row')) {
|
||||
var pdep = parseInt(prev.dataset.mmDep || '0', 10);
|
||||
if (pdep === draggedDep - 1) {
|
||||
return parseInt(prev.dataset.mmIdx || '0', 10);
|
||||
}
|
||||
if (pdep < draggedDep - 1) {
|
||||
return 0; // 유효한 상위가 없음
|
||||
}
|
||||
}
|
||||
prev = prev.previousElementSibling;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// 현재 화면 순서 그대로 구조(순서/상위/깊이) 페이로드를 만든다.
|
||||
const buildTreePayload = function() {
|
||||
var arr = [];
|
||||
tbody.querySelectorAll('tr.menu-row').forEach(function(row){
|
||||
var idInput = row.querySelector('input[name="mm_idx[]"]');
|
||||
if (!idInput) return;
|
||||
arr.push({
|
||||
idx: parseInt(idInput.value, 10),
|
||||
pidx: parseInt(row.dataset.mmPidx || '0', 10),
|
||||
dep: parseInt(row.dataset.mmDep || '0', 10)
|
||||
});
|
||||
});
|
||||
return arr;
|
||||
};
|
||||
const payloadInput = document.getElementById('menu-tree-payload');
|
||||
// 수동 "순서 적용" 버튼 제출 시에도 구조가 반영되도록 payload 채움
|
||||
moveForm.addEventListener('submit', function(){
|
||||
if (payloadInput) payloadInput.value = JSON.stringify(buildTreePayload());
|
||||
});
|
||||
|
||||
var onMouseUp = function() {
|
||||
if (!draggingActive || !draggingRow || !placeholderRow) {
|
||||
clearDragState();
|
||||
return;
|
||||
}
|
||||
var prevRow = placeholderRow.previousElementSibling;
|
||||
tbody.insertBefore(draggingRow, placeholderRow);
|
||||
refreshOrderNos();
|
||||
|
||||
var draggedDep = parseInt(draggingRow.dataset.mmDep || '0', 10);
|
||||
var draggedPidx = parseInt(draggingRow.dataset.mmPidx || '0', 10);
|
||||
|
||||
if (draggedDep > 0) {
|
||||
var valid = false;
|
||||
if (prevRow && prevRow.classList.contains('menu-row')) {
|
||||
var prevIdx = parseInt(prevRow.dataset.mmIdx || '0', 10);
|
||||
var prevPidx = parseInt(prevRow.dataset.mmPidx || '0', 10);
|
||||
if (prevRow === draggingRow) {
|
||||
valid = true;
|
||||
} else if (prevIdx === draggedPidx || prevPidx === draggedPidx) {
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
if (!valid) {
|
||||
alert('하위 메뉴는 다른 상위 메뉴에는 들어갈 수 없습니다.');
|
||||
// 하위 메뉴: 놓인 위치의 상위 메뉴를 새 부모로 재지정 (다른 상위 메뉴로 이동 허용)
|
||||
var newPidx = resolveNewParentIdx(draggingRow, draggedDep);
|
||||
if (newPidx <= 0) {
|
||||
alert('하위 메뉴는 상위 메뉴 아래에만 놓을 수 있습니다.');
|
||||
restoreOrderByIds(originalOrderIds);
|
||||
clearDragState();
|
||||
return;
|
||||
}
|
||||
draggingRow.dataset.mmPidx = String(newPidx);
|
||||
}
|
||||
|
||||
const approved = window.confirm('변경한 순서를 적용할까요?');
|
||||
if (!approved) {
|
||||
// 취소 시 원래 순서·상위로 되돌림
|
||||
if (typeof draggingRow.dataset.mmPidxOrig !== 'undefined') {
|
||||
draggingRow.dataset.mmPidx = draggingRow.dataset.mmPidxOrig;
|
||||
}
|
||||
restoreOrderByIds(originalOrderIds);
|
||||
} else {
|
||||
if (payloadInput) payloadInput.value = JSON.stringify(buildTreePayload());
|
||||
moveForm.submit();
|
||||
}
|
||||
clearDragState();
|
||||
@@ -511,6 +579,8 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
||||
e.preventDefault();
|
||||
originalOrderIds = collectCurrentOrderIds();
|
||||
draggingRow = row;
|
||||
// 취소 시 되돌리기 위해 원래 상위(부모) 보관
|
||||
row.dataset.mmPidxOrig = row.dataset.mmPidx || '0';
|
||||
placeholderRow = makePlaceholder();
|
||||
draggingActive = true;
|
||||
row.classList.add('dragging');
|
||||
@@ -518,6 +588,7 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
||||
tbody.insertBefore(placeholderRow, row.nextSibling);
|
||||
lastClientY = e.clientY;
|
||||
updatePlaceholderPosition();
|
||||
autoScrollRaf = window.requestAnimationFrame(autoScrollTick);
|
||||
window.addEventListener('mousemove', onMouseMove, { passive: true });
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ for ($year = $baseYear - 2; $year <= $baseYear + 2; $year++) {
|
||||
?>
|
||||
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">LOT-No 디스켓 불출</span>
|
||||
<span class="text-sm font-bold text-gray-700">발주파일 생성</span>
|
||||
</section>
|
||||
|
||||
<section class="border border-gray-300 rounded-lg p-2 mt-2 bg-white">
|
||||
@@ -92,11 +92,12 @@ for ($year = $baseYear - 2; $year <= $baseYear + 2; $year++) {
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<form method="post" action="<?= base_url('bag/order/lot-seed/generate') ?>" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="bo_idx" value="<?= $boIdx ?>" />
|
||||
<button type="submit" class="text-blue-600 hover:underline text-xs">seed 생성</button>
|
||||
</form>
|
||||
<button type="button"
|
||||
class="lot-preview-btn text-blue-600 hover:underline text-xs"
|
||||
data-bo-idx="<?= $boIdx ?>"
|
||||
data-lot="<?= esc((string) ($order->bo_lot_no ?? ''), 'attr') ?>">
|
||||
내용 확인
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-center"><?= esc((string) ($order->bo_order_date ?? '')) ?></td>
|
||||
<td class="text-center font-mono"><?= esc((string) ($order->bo_lot_no ?? '')) ?></td>
|
||||
@@ -128,3 +129,133 @@ for ($year = $baseYear - 2; $year <= $baseYear + 2; $year++) {
|
||||
<?php if (isset($pager)): ?>
|
||||
<div class="mt-2 mb-2 no-print"><?= $pager->links() ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 발주파일 생성 전 '내용 확인' 모달 -->
|
||||
<div id="lot-preview-overlay" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/40 p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-3xl max-h-[85vh] flex flex-col">
|
||||
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-2">
|
||||
<span class="text-sm font-bold text-gray-800">발주파일 내용 확인</span>
|
||||
<button type="button" id="lot-preview-close" class="text-gray-400 hover:text-gray-700 text-lg leading-none">×</button>
|
||||
</div>
|
||||
<div class="p-4 overflow-auto text-sm">
|
||||
<div id="lot-preview-loading" class="text-center text-gray-400 py-8">불러오는 중…</div>
|
||||
<div id="lot-preview-error" class="hidden text-center text-red-600 py-8"></div>
|
||||
<div id="lot-preview-body" class="hidden">
|
||||
<p class="text-xs text-gray-500 mb-2">아래 내용으로 <b>암호화된 발주파일(.seed.json)</b>이 생성·다운로드됩니다. 생성 전 내용을 확인하세요.</p>
|
||||
<dl class="grid grid-cols-[6rem_1fr] gap-y-1 text-[13px] mb-3 border border-gray-200 rounded p-2 bg-gray-50">
|
||||
<dt class="font-semibold text-gray-600">LOT-No</dt><dd class="font-mono" id="lp-lot"></dd>
|
||||
<dt class="font-semibold text-gray-600">발주일</dt><dd id="lp-date"></dd>
|
||||
<dt class="font-semibold text-gray-600">제작업체</dt><dd id="lp-company"></dd>
|
||||
<dt class="font-semibold text-gray-600">버전</dt><dd id="lp-version"></dd>
|
||||
<dt class="font-semibold text-gray-600">무결성해시</dt><dd class="font-mono text-[11px] text-gray-500 break-all" id="lp-hash"></dd>
|
||||
</dl>
|
||||
<div class="overflow-auto border border-gray-200 rounded">
|
||||
<table class="w-full data-table text-[13px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left pl-2">봉투코드</th>
|
||||
<th class="text-left">봉투명</th>
|
||||
<th class="w-20 text-right">단가</th>
|
||||
<th class="w-16 text-right">박스</th>
|
||||
<th class="w-16 text-right">팩</th>
|
||||
<th class="w-20 text-right">낱장</th>
|
||||
<th class="w-24 text-right pr-2">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="lp-items"></tbody>
|
||||
<tfoot>
|
||||
<tr class="font-semibold bg-gray-50">
|
||||
<td class="pl-2" colspan="3">합계 (<span id="lp-line-count">0</span>품목)</td>
|
||||
<td class="text-right" id="lp-sum-box">0</td>
|
||||
<td class="text-right">—</td>
|
||||
<td class="text-right" id="lp-sum-sheet">0</td>
|
||||
<td class="text-right pr-2" id="lp-sum-amount">0</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 px-4 py-2 flex items-center justify-end gap-2">
|
||||
<button type="button" id="lot-preview-cancel" class="border border-gray-300 text-gray-600 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50">닫기</button>
|
||||
<form method="post" action="<?= base_url('bag/order/lot-seed/generate') ?>" class="inline" id="lot-generate-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="bo_idx" id="lot-generate-bo-idx" value="" />
|
||||
<button type="submit" id="lot-generate-submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 disabled:opacity-50" disabled>발주파일 생성(다운로드)</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var previewUrl = '<?= base_url('bag/order/lot-seed/preview') ?>';
|
||||
var overlay = document.getElementById('lot-preview-overlay');
|
||||
var elLoading = document.getElementById('lot-preview-loading');
|
||||
var elError = document.getElementById('lot-preview-error');
|
||||
var elBody = document.getElementById('lot-preview-body');
|
||||
var elSubmit = document.getElementById('lot-generate-submit');
|
||||
var elBoIdx = document.getElementById('lot-generate-bo-idx');
|
||||
var nf = function (n) { return new Intl.NumberFormat('ko-KR').format(n || 0); };
|
||||
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>' })[c]; }); };
|
||||
|
||||
function openModal() { overlay.classList.remove('hidden'); overlay.classList.add('flex'); }
|
||||
function closeModal() { overlay.classList.add('hidden'); overlay.classList.remove('flex'); }
|
||||
|
||||
function showState(state) {
|
||||
elLoading.classList.toggle('hidden', state !== 'loading');
|
||||
elError.classList.toggle('hidden', state !== 'error');
|
||||
elBody.classList.toggle('hidden', state !== 'body');
|
||||
elSubmit.disabled = state !== 'body';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.lot-preview-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var boIdx = btn.getAttribute('data-bo-idx');
|
||||
elBoIdx.value = boIdx;
|
||||
openModal();
|
||||
showState('loading');
|
||||
fetch(previewUrl + '?bo_idx=' + encodeURIComponent(boIdx), { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (!d || !d.ok) {
|
||||
elError.textContent = (d && d.error) || '내용을 불러오지 못했습니다.';
|
||||
showState('error');
|
||||
return;
|
||||
}
|
||||
document.getElementById('lp-lot').textContent = d.lot_no || '';
|
||||
document.getElementById('lp-date').textContent = d.order_date || '';
|
||||
document.getElementById('lp-company').textContent = d.company || '-';
|
||||
document.getElementById('lp-version').textContent = 'v' + (d.version || 1);
|
||||
document.getElementById('lp-hash').textContent = d.order_hash || '';
|
||||
var rows = (d.items || []).map(function (it) {
|
||||
return '<tr>'
|
||||
+ '<td class="pl-2 font-mono">' + esc(it.bag_code) + '</td>'
|
||||
+ '<td>' + esc(it.bag_name) + '</td>'
|
||||
+ '<td class="text-right">' + nf(it.unit_price) + '</td>'
|
||||
+ '<td class="text-right">' + nf(it.qty_box) + '</td>'
|
||||
+ '<td class="text-right">' + nf(it.qty_pack) + '</td>'
|
||||
+ '<td class="text-right">' + nf(it.qty_sheet) + '</td>'
|
||||
+ '<td class="text-right pr-2">' + nf(it.amount) + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
document.getElementById('lp-items').innerHTML = rows || '<tr><td colspan="7" class="text-center text-gray-400 py-4">품목이 없습니다.</td></tr>';
|
||||
var t = d.totals || {};
|
||||
document.getElementById('lp-line-count').textContent = nf(t.line_count);
|
||||
document.getElementById('lp-sum-box').textContent = nf(t.qty_box);
|
||||
document.getElementById('lp-sum-sheet').textContent = nf(t.qty_sheet);
|
||||
document.getElementById('lp-sum-amount').textContent = nf(t.amount);
|
||||
showState('body');
|
||||
})
|
||||
.catch(function () {
|
||||
elError.textContent = '통신 오류가 발생했습니다.';
|
||||
showState('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('lot-preview-close').addEventListener('click', closeModal);
|
||||
document.getElementById('lot-preview-cancel').addEventListener('click', closeModal);
|
||||
overlay.addEventListener('click', function (e) { if (e.target === overlay) closeModal(); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-9 gap-3 mt-2">
|
||||
<section class="xl:col-span-5 border border-gray-300 rounded-lg bg-white overflow-hidden flex flex-col">
|
||||
<div id="order-split" class="flex flex-col xl:flex-row gap-3 mt-2 items-stretch">
|
||||
<section id="list-card" class="border border-gray-300 rounded-lg bg-white overflow-hidden flex flex-col min-w-0 xl:shrink-0">
|
||||
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700">접수 리스트(전화)</div>
|
||||
<div id="list-scroll" class="overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
@@ -43,7 +43,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="detail-card" class="xl:col-span-4 border border-gray-300 rounded-lg bg-white overflow-hidden">
|
||||
<!-- 드래그 가능한 너비 조절 손잡이 (xl 이상 좌우 배치일 때만 표시) -->
|
||||
<div id="split-handle" class="hidden xl:flex items-center justify-center cursor-col-resize select-none shrink-0" style="width:8px;touch-action:none;" role="separator" aria-orientation="vertical" title="드래그하여 표 너비를 조절합니다">
|
||||
<div class="w-1.5 h-16 rounded-full bg-gray-300 hover:bg-blue-400 transition-colors"></div>
|
||||
</div>
|
||||
|
||||
<section id="detail-card" class="border border-gray-300 rounded-lg bg-white overflow-hidden min-w-0 xl:flex-1">
|
||||
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700">지정판매소 정보</div>
|
||||
|
||||
<form id="order-detail-form" action="<?= base_url('bag/order/phone/manage/update') ?>" method="POST" class="p-3 space-y-3">
|
||||
@@ -119,15 +124,15 @@
|
||||
<span class="font-semibold text-gray-700">포장 명세</span>
|
||||
<span class="text-gray-600">포장량: <span id="packing-total" class="font-bold text-blue-700">0</span> 개</span>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded-lg overflow-auto max-h-[179px]">
|
||||
<div id="packing-scroll" class="border border-gray-300 rounded-lg overflow-auto max-h-[179px]">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12 text-center sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">No</th>
|
||||
<th class="w-64 text-left sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투종류</th>
|
||||
<th class="w-28 text-center sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투코드</th>
|
||||
<th class="w-20 text-right sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">수량</th>
|
||||
<th class="text-left sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">포장</th>
|
||||
<th class="w-10 text-center sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">No</th>
|
||||
<th class="w-40 text-left sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투종류</th>
|
||||
<th class="text-left sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투코드</th>
|
||||
<th class="w-16 text-right sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">수량</th>
|
||||
<th class="w-16 text-center sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">포장</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="packing-body">
|
||||
@@ -446,6 +451,9 @@
|
||||
// 현재 이 주문을 보고 있으면 상세·포장명세 즉시 갱신
|
||||
if (String(selectedId) === String(data.so_idx)) {
|
||||
renderPacking(order);
|
||||
// 새로 스캔한 봉투가 항상 맨 위(최신)에 보이도록 스크롤을 맨 위로 이동
|
||||
const packingScrollEl = document.getElementById('packing-scroll');
|
||||
if (packingScrollEl) packingScrollEl.scrollTop = 0;
|
||||
const cell = detailBody.querySelector('.item-packed-cell[data-bag-code="' + (window.CSS && CSS.escape ? CSS.escape(String(data.bag_code)) : String(data.bag_code)) + '"]');
|
||||
if (cell) {
|
||||
cell.textContent = nf(Number(data.packed_qty || 0));
|
||||
@@ -491,6 +499,74 @@
|
||||
new ResizeObserver(function () { syncListHeight(); }).observe(detailCardEl);
|
||||
}
|
||||
|
||||
// ── 접수 리스트 ↔ 지정판매소 정보 사이 너비 조절(드래그 스플리터) ──────
|
||||
// 손잡이를 접수 리스트 쪽(왼쪽)으로 끌면 리스트 폭이 줄고 지정판매소 정보 폭이 넓어진다.
|
||||
const splitEl = document.getElementById('order-split');
|
||||
const listCardEl = document.getElementById('list-card');
|
||||
const splitHandleEl = document.getElementById('split-handle');
|
||||
const SPLIT_KEY = 'jrj_order_split_pct';
|
||||
const SPLIT_MIN = 25, SPLIT_MAX = 75, SPLIT_DEFAULT = 55.5; // 기본값 ≈ 기존 5:4 비율
|
||||
const xlMedia = window.matchMedia('(min-width: 1280px)');
|
||||
function clampPct(p) { return Math.min(SPLIT_MAX, Math.max(SPLIT_MIN, p)); }
|
||||
function currentPct() {
|
||||
const saved = parseFloat(localStorage.getItem(SPLIT_KEY) || '');
|
||||
return clampPct(isFinite(saved) ? saved : SPLIT_DEFAULT);
|
||||
}
|
||||
function applySplit(pct) {
|
||||
if (!listCardEl) return;
|
||||
if (xlMedia.matches) {
|
||||
// 좌우 배치일 때만 폭 지정(세로로 쌓이는 좁은 화면에서는 전체폭 사용)
|
||||
listCardEl.style.flex = '0 0 ' + pct + '%';
|
||||
listCardEl.style.maxWidth = pct + '%';
|
||||
} else {
|
||||
listCardEl.style.flex = '';
|
||||
listCardEl.style.maxWidth = '';
|
||||
}
|
||||
syncListHeight();
|
||||
}
|
||||
if (splitHandleEl && splitEl && listCardEl) {
|
||||
let dragging = false, rafId = 0, pendingPct = null;
|
||||
function onMove(clientX) {
|
||||
const rect = splitEl.getBoundingClientRect();
|
||||
if (rect.width <= 0) return;
|
||||
pendingPct = clampPct((clientX - rect.left) / rect.width * 100);
|
||||
if (!rafId) {
|
||||
rafId = requestAnimationFrame(function () {
|
||||
rafId = 0;
|
||||
if (pendingPct != null) applySplit(pendingPct);
|
||||
});
|
||||
}
|
||||
}
|
||||
function pointerMove(e) { if (dragging) { e.preventDefault(); onMove(e.clientX); } }
|
||||
function pointerUp() {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
window.removeEventListener('pointermove', pointerMove);
|
||||
window.removeEventListener('pointerup', pointerUp);
|
||||
if (pendingPct != null) localStorage.setItem(SPLIT_KEY, String(Math.round(pendingPct * 10) / 10));
|
||||
}
|
||||
splitHandleEl.addEventListener('pointerdown', function (e) {
|
||||
if (!xlMedia.matches) return; // 좁은 화면에서는 비활성
|
||||
e.preventDefault();
|
||||
dragging = true;
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = 'col-resize';
|
||||
window.addEventListener('pointermove', pointerMove);
|
||||
window.addEventListener('pointerup', pointerUp);
|
||||
});
|
||||
// 더블클릭 시 기본 비율로 초기화
|
||||
splitHandleEl.addEventListener('dblclick', function () {
|
||||
localStorage.removeItem(SPLIT_KEY);
|
||||
applySplit(SPLIT_DEFAULT);
|
||||
});
|
||||
// 브레이크포인트 전환 시 폭 지정 적용/해제
|
||||
(xlMedia.addEventListener ? xlMedia.addEventListener('change', function () { applySplit(currentPct()); })
|
||||
: xlMedia.addListener(function () { applySplit(currentPct()); }));
|
||||
applySplit(currentPct());
|
||||
}
|
||||
|
||||
// ── 포장 명세 탭: 주문 품목을 박스/팩/낱장 단위 행으로 펼침 ──────────
|
||||
const packingBody = document.getElementById('packing-body');
|
||||
const packingTotal = document.getElementById('packing-total');
|
||||
@@ -508,10 +584,10 @@
|
||||
const qty = parseInt(s.qty || 0, 10) || 0;
|
||||
totalQty += qty;
|
||||
return '<tr><td class="text-center">' + no + '</td>'
|
||||
+ '<td class="text-left pl-2">' + packEsc(code) + ' ' + packEsc(name) + '</td>'
|
||||
+ '<td class="text-center font-mono text-gray-600">' + packEsc(s.code || '') + '</td>'
|
||||
+ '<td class="text-left pl-2 truncate" title="' + packEsc(code) + ' ' + packEsc(name) + '">' + packEsc(code) + ' ' + packEsc(name) + '</td>'
|
||||
+ '<td class="text-left pl-2 font-mono text-gray-600">' + packEsc(s.code || '') + '</td>'
|
||||
+ '<td class="text-right pr-2">' + nf(qty) + '</td>'
|
||||
+ '<td class="text-left pl-2">' + packEsc(s.unit || '') + '</td></tr>';
|
||||
+ '<td class="text-center">' + packEsc(s.unit || '') + '</td></tr>';
|
||||
});
|
||||
packingBody.innerHTML = rows.length ? rows.join('')
|
||||
: '<tr><td colspan="5" class="text-center py-6 text-gray-400">스캔된 포장 내역이 없습니다.</td></tr>';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- LOT-No 디스켓 불출 소메뉴 링크 보정
|
||||
-- 발주파일 생성(구 LOT-No 디스켓 불출) 소메뉴 링크 보정
|
||||
-- 실행:
|
||||
-- mysql --default-character-set=utf8mb4 -u ... -p DBNAME < writable/database/menu_link_lot_seed_issue.sql
|
||||
|
||||
@@ -8,4 +8,4 @@ SET CHARACTER_SET_CLIENT = utf8mb4;
|
||||
UPDATE `menu` m
|
||||
INNER JOIN `menu_type` t ON t.mt_idx = m.mt_idx AND t.mt_code = 'site'
|
||||
SET m.mm_link = 'bag/order/lot-seed'
|
||||
WHERE m.mm_name = 'LOT-No 디스켓 불출';
|
||||
WHERE m.mm_name IN ('발주파일 생성', 'LOT-No 디스켓 불출');
|
||||
|
||||
@@ -27,7 +27,7 @@ SET m.mm_link = CASE m.mm_name
|
||||
WHEN '지정 판매소 현황' THEN 'bag/designated-shops/district-new-cancel'
|
||||
WHEN '발주 등록' THEN 'bag/order/create'
|
||||
WHEN '발주 변경' THEN 'bag/order/change'
|
||||
WHEN 'LOT-No 디스켓 불출' THEN 'bag/order/lot-seed'
|
||||
WHEN '발주파일 생성' THEN 'bag/order/lot-seed'
|
||||
WHEN '발주 현황' THEN 'bag/bag-orders'
|
||||
WHEN '발주 입고[스캐너]' THEN 'bag/receiving/create'
|
||||
WHEN '입고 현황' THEN 'bag/bag-receivings'
|
||||
|
||||
@@ -83,7 +83,7 @@ SELECT @mt_site, 1, t.mm_name,
|
||||
CASE t.mm_name
|
||||
WHEN '발주 등록' THEN 'bag/order/create'
|
||||
WHEN '발주 변경' THEN 'bag/order/change'
|
||||
WHEN 'LOT-No 디스켓 불출' THEN 'bag/order/lot-seed'
|
||||
WHEN '발주파일 생성' THEN 'bag/order/lot-seed'
|
||||
WHEN '발주 현황' THEN 'bag/bag-orders'
|
||||
WHEN '발주 입고[스캐너]' THEN 'bag/receiving/create'
|
||||
WHEN '입고 현황' THEN 'bag/bag-receivings'
|
||||
@@ -93,7 +93,7 @@ SELECT @mt_site, 1, t.mm_name,
|
||||
FROM (
|
||||
SELECT 0 AS mm_num, '발주 등록' AS mm_name UNION ALL
|
||||
SELECT 1, '발주 변경' UNION ALL
|
||||
SELECT 2, 'LOT-No 디스켓 불출' UNION ALL
|
||||
SELECT 2, '발주파일 생성' UNION ALL
|
||||
SELECT 3, '발주 현황' UNION ALL
|
||||
SELECT 4, '발주 입고[스캐너]' UNION ALL
|
||||
SELECT 5, '일괄입고' UNION ALL
|
||||
|
||||
13
writable/database/rename_lot_seed_menu_to_order_file.sql
Normal file
13
writable/database/rename_lot_seed_menu_to_order_file.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- 메뉴명 변경: 'LOT-No 디스켓 불출' → '발주파일 생성'
|
||||
-- 링크(mm_link='bag/order/lot-seed') 기준으로, 기본 이름을 쓰는 항목만 안전하게 변경한다.
|
||||
-- (지자체가 직접 다른 이름으로 바꿔 둔 항목은 건드리지 않음)
|
||||
-- 실행:
|
||||
-- mysql --default-character-set=utf8mb4 -u USER -p DBNAME < writable/database/rename_lot_seed_menu_to_order_file.sql
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
SET CHARACTER_SET_CLIENT = utf8mb4;
|
||||
|
||||
UPDATE `menu`
|
||||
SET `mm_name` = '발주파일 생성'
|
||||
WHERE `mm_link` = 'bag/order/lot-seed'
|
||||
AND `mm_name` = 'LOT-No 디스켓 불출';
|
||||
Reference in New Issue
Block a user