화면별 매뉴얼·이 화면 설명 버튼·탭 새로고침/경고·최근방문 기록 보강.
- 매뉴얼: 화면(소메뉴)별 용어·버튼·필드 설명으로 확장 + 기본정보 페이지 신규, 개요에 용어 사전 추가 (종량제 지식 없는 사용자 대상) - "이 화면 설명" 버튼: 화면 경로→매뉴얼 매핑(Config\Manual::screenHelp, manual_help_url_for_path). 워크스페이스 탭은 새 탭으로, 직접 페이지는 새 창으로 - 워크스페이스: 개별 탭 새로고침(↻) 버튼, 탭 2개 이상일 때만 새로고침 경고, 사이드바 하단 링크(매뉴얼 등)도 탭으로 열기 - 임베드: 탭 내 링크/폼 embed 유지(중첩 헤더 방지), 매뉴얼 리다이렉트 embed 유지 - 사이드바 하단: 종합그래프 → 사용자 매뉴얼 링크 - 최근 방문 메뉴: embed 페이지에도 방문 기록, 대시보드는 storage 이벤트로 실시간 갱신 - E2E qa_sweep 추가(주요 화면 콘솔/오버레이/매뉴얼/도움말 매핑 점검) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,9 +26,46 @@ class Manual extends BaseConfig
|
|||||||
'flow' => ['title' => '핵심 업무 흐름', 'file' => '10_workflow.md'],
|
'flow' => ['title' => '핵심 업무 흐름', 'file' => '10_workflow.md'],
|
||||||
'order' => ['title' => '발주·입고', 'file' => '20_order_receiving.md'],
|
'order' => ['title' => '발주·입고', 'file' => '20_order_receiving.md'],
|
||||||
'inventory' => ['title' => '재고·실사', 'file' => '30_inventory.md'],
|
'inventory' => ['title' => '재고·실사', 'file' => '30_inventory.md'],
|
||||||
'sales' => ['title' => '판매·불출', 'file' => '40_sales_issue.md'],
|
'sales' => ['title' => '판매·반품·불출·주문', 'file' => '40_sales_issue.md'],
|
||||||
'reports' => ['title' => '판매현황·수불·통계', 'file' => '50_reports.md'],
|
'reports' => ['title' => '현황·리포트·수불', 'file' => '50_reports.md'],
|
||||||
|
'basic' => ['title' => '기본정보(판매소·단가·코드)', 'file' => '60_basic_info.md'],
|
||||||
'codes' => ['title' => '봉투·LOT·바코드 코드체계', 'file' => '90_code_system.md'],
|
'codes' => ['title' => '봉투·LOT·바코드 코드체계', 'file' => '90_code_system.md'],
|
||||||
'faq' => ['title' => '자주 묻는 질문·문의', 'file' => '99_faq.md'],
|
'faq' => ['title' => '자주 묻는 질문·문의', 'file' => '99_faq.md'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 화면 경로(접두) → 그 화면을 설명하는 매뉴얼 slug.
|
||||||
|
* "이 화면 설명" 버튼이 현재 경로로 알맞은 매뉴얼 페이지를 연다.
|
||||||
|
* 더 긴(구체적) 접두가 우선하도록 길이 내림차순으로 매칭한다.
|
||||||
|
*
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
public array $screenHelp = [
|
||||||
|
'bag/order/phone' => 'sales',
|
||||||
|
'bag/order' => 'order',
|
||||||
|
'bag/bag-orders' => 'order',
|
||||||
|
'bag/receiving' => 'order',
|
||||||
|
'bag/bag-receivings' => 'order',
|
||||||
|
'bag/inventory' => 'inventory',
|
||||||
|
'bag/sale' => 'sales',
|
||||||
|
'bag/sales' => 'sales',
|
||||||
|
'bag/issue' => 'sales',
|
||||||
|
'bag/bag-issues' => 'sales',
|
||||||
|
'bag/bag-sales' => 'sales',
|
||||||
|
'bag/shop-orders' => 'sales',
|
||||||
|
'bag/flow' => 'reports',
|
||||||
|
'bag/reports' => 'reports',
|
||||||
|
'bag/analytics' => 'reports',
|
||||||
|
'bag/designated-shops' => 'basic',
|
||||||
|
'bag/bag-prices' => 'basic',
|
||||||
|
'bag/prices' => 'basic',
|
||||||
|
'bag/packaging-units' => 'basic',
|
||||||
|
'bag/code-kinds' => 'basic',
|
||||||
|
'bag/code-details' => 'basic',
|
||||||
|
'bag/managers' => 'basic',
|
||||||
|
'bag/companies' => 'basic',
|
||||||
|
'bag/sales-agencies' => 'basic',
|
||||||
|
'bag/free-recipients' => 'basic',
|
||||||
|
'bag/number-lookup' => 'codes',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3580,8 +3580,12 @@ SQL);
|
|||||||
public function manual(): \CodeIgniter\HTTP\RedirectResponse
|
public function manual(): \CodeIgniter\HTTP\RedirectResponse
|
||||||
{
|
{
|
||||||
$first = (new \App\Libraries\ManualRenderer())->firstSlug();
|
$first = (new \App\Libraries\ManualRenderer())->firstSlug();
|
||||||
|
$url = site_url('bag/manual/' . $first);
|
||||||
|
if ($this->isEmbeddedRequest()) {
|
||||||
|
$url .= '?embed=1'; // 워크스페이스 탭 안에서는 임베드 유지(중첩 헤더 방지)
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->to(site_url('bag/manual/' . $first));
|
return redirect()->to($url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,49 +1,48 @@
|
|||||||
# 시작하기 · 시스템 개요
|
# 시작하기 · 시스템 개요
|
||||||
|
|
||||||
종량제 쓰레기봉투 물류 시스템 사용자 매뉴얼입니다. 이 문서는 실제 업무를 처리하는 담당자(지자체 관리자·지정판매소)를 위한 **빠른 시작 안내서**입니다.
|
종량제 쓰레기봉투 물류 시스템 사용자 매뉴얼입니다. **이 시스템을 처음 쓰는 분**도 화면을 이해하고 업무를 처리할 수 있도록, 화면마다 쓰이는 용어와 버튼의 의미를 설명합니다.
|
||||||
|
|
||||||
## 1. 시스템은 무엇을 하나요?
|
## 1. 이 시스템은 무엇을 하나요?
|
||||||
|
|
||||||
지자체 종량제 쓰레기봉투의 **발주 → 입고 → 재고 → 판매/불출 → 정산·통계** 전 과정을 관리합니다.
|
지자체가 주민에게 파는 **종량제 쓰레기봉투**가 ① 제작업체에 **주문(발주)** 되고 → ② 창고로 **들어오고(입고)** → ③ **재고**로 보관되다가 → ④ 동네 가게(지정판매소)에 **팔리거나(판매)** 무료 대상자에게 **나눠지는(불출)** 전 과정을 컴퓨터로 관리합니다. 마지막엔 ⑤ 얼마나 팔렸는지 **집계·정산(현황/리포트)** 합니다.
|
||||||
|
|
||||||
- 봉투를 제작업체에 **발주**하고, 들어온 물량을 **입고** 처리합니다.
|
## 2. 꼭 알아야 할 기본 용어 (용어 사전)
|
||||||
- 현재 **재고**를 확인하고, 실사로 실제 수량과 맞춥니다.
|
|
||||||
- 지정판매소에 **판매**하거나 무료 대상자에게 **불출**합니다.
|
|
||||||
- 일계표·기간별·연간 등 **판매현황**과 **수불·통계**를 조회합니다.
|
|
||||||
|
|
||||||
## 2. 로그인과 화면 구성
|
| 용어 | 쉬운 설명 |
|
||||||
|
|---|---|
|
||||||
|
| **발주** | 봉투를 제작업체에 "이만큼 만들어 주세요"라고 **주문**하는 것 |
|
||||||
|
| **입고** | 주문한 봉투가 창고에 **도착해 들여놓는** 것 |
|
||||||
|
| **재고** | 지금 창고에 **남아 있는 봉투 수량** |
|
||||||
|
| **불출** | 봉투를 창고에서 **꺼내 내보내는** 것 (주로 무료 배부) |
|
||||||
|
| **수불(受拂)** | **들어오고(수입)·나가는(불출)** 움직임을 적은 장부 |
|
||||||
|
| **지정판매소** | 봉투를 파는 **동네 가게**(편의점·마트 등) |
|
||||||
|
| **대행소(판매대행소)** | 봉투 **배송·유통을 대행**하는 업체 |
|
||||||
|
| **실사** | 컴퓨터 기록과 **실제 창고 수량이 맞는지 직접 세어 확인**하는 것 |
|
||||||
|
| **박스 / 팩 / 낱장** | 포장 단위. **박스 > 팩 > 낱장(봉투 1장)**. 1박스 = 여러 팩, 1팩 = 여러 낱장 |
|
||||||
|
| **LOT(로트)** | 한 번의 발주 묶음에 부여되는 **추적용 일련번호** |
|
||||||
|
| **바코드(봉투번호)** | 박스·팩·낱장마다 붙는 **고유 번호**(스캔용) |
|
||||||
|
| **무료용 / 공공용** | 주민 무료 배부용 / 공공기관 사용용 봉투 구분 |
|
||||||
|
|
||||||
1. 발급받은 아이디·비밀번호로 로그인합니다.
|
> 박스·팩·낱장·LOT·바코드의 자세한 규칙은 좌측 목차 **[봉투·LOT·바코드 코드체계]** 참고.
|
||||||
2. 관리자(지자체/슈퍼)는 보안을 위해 **2차 인증(OTP)** 이 적용될 수 있습니다.
|
|
||||||
3. 로그인하면 상단에 **대메뉴**가 보이고, 각 대메뉴에 마우스를 올리면 **소메뉴**가 펼쳐집니다.
|
|
||||||
4. 화면 상단의 제목 바에 현재 보고 있는 화면 이름이 표시됩니다.
|
|
||||||
|
|
||||||
> 슈퍼 관리자는 작업할 **지자체를 먼저 선택**해야 업무 화면이 열립니다.
|
## 3. 화면 구성과 사용법
|
||||||
|
|
||||||
## 3. 사용자 역할(권한)
|
- 로그인하면 **워크스페이스**(탭 작업공간)가 열립니다. 상단에 대분류 메뉴, **대분류를 클릭하면 왼쪽에 소메뉴**가 펼쳐집니다.
|
||||||
|
- 메뉴를 클릭하면 **탭으로 열립니다.** 여러 화면을 열어두고 전환해도 입력하던 내용이 유지됩니다.
|
||||||
|
- 각 화면에서 **"이 화면 설명"(❓) 버튼**을 누르면, 그 화면에 해당하는 매뉴얼이 새 탭으로 열립니다.
|
||||||
|
|
||||||
시스템은 4단계 역할로 접근 권한을 구분합니다.
|
## 4. 사용자 역할(권한)
|
||||||
|
|
||||||
| 레벨 | 역할 | 할 수 있는 일 |
|
| 역할 | 할 수 있는 일 |
|
||||||
|---|---|---|
|
|---|---|
|
||||||
| 1 | 일반 사용자 | 기본 조회 |
|
| 일반 사용자 | 기본 조회 |
|
||||||
| 2 | 지정판매소 | 봉투 판매·반품, 자기 판매 현황 조회 |
|
| 지정판매소 | 봉투 판매·반품, 자기 판매 현황 조회 |
|
||||||
| 3 | 지자체 관리자 | 소속 지자체의 발주·입고·재고·불출·판매·리포트 전반 |
|
| 지자체 관리자 | 소속 지자체의 발주·입고·재고·불출·판매·리포트 전반 |
|
||||||
| 4 | 슈퍼 관리자 | 전체 시스템 관리 + 기본코드 등 마스터 관리 (지자체 선택 후 작업) |
|
| 슈퍼 관리자 | 전체 + 기본코드 등 마스터 관리(지자체 선택 후 작업) |
|
||||||
|
|
||||||
### 역할별 접근 한눈에 보기
|
## 5. 화면별 설명은 어디에?
|
||||||
|
|
||||||
| 기능군 | 일반 | 판매소 | 지자체관리자 | 슈퍼 |
|
좌측 목차에서 업무군을 고르면 그 안에 **화면(소메뉴)별 설명**이 있습니다.
|
||||||
|---|:--:|:--:|:--:|:--:|
|
- **발주·입고** / **재고·실사** / **판매·반품·불출·주문** / **현황·리포트·수불** / **기본정보(판매소·단가·코드)**
|
||||||
| 기본정보 조회(코드·단가·포장) | △ | ○ | ○ | ○ |
|
|
||||||
| 기본코드·마스터 편집 | ✕ | ✕ | △(지자체분) | ○ |
|
|
||||||
| 발주·입고·재고·불출 | ✕ | ✕ | ○ | ○ |
|
|
||||||
| 판매·반품 등록 | ✕ | ○ | ○ | ○ |
|
|
||||||
| 판매현황·수불·통계 | ✕ | △(자기분) | ○ | ○ |
|
|
||||||
|
|
||||||
(○ 사용 가능 · △ 제한적 · ✕ 불가)
|
각 화면 설명은 **그 화면 고유의 용어·입력 항목·버튼·작업 순서**만 담았습니다.
|
||||||
|
|
||||||
## 4. 다음 단계
|
|
||||||
|
|
||||||
- 전체 업무가 어떻게 이어지는지 먼저 보려면 **[핵심 업무 흐름]** 으로 이동하세요.
|
|
||||||
- 특정 작업 방법은 좌측 목차에서 해당 항목(발주·입고 / 재고·실사 / 판매·불출 / 판매현황·수불·통계)을 선택하세요.
|
|
||||||
|
|||||||
@@ -1,41 +1,66 @@
|
|||||||
# 발주 · 입고
|
# 발주 · 입고
|
||||||
|
|
||||||
제작업체에 봉투를 주문(발주)하고, 도착한 물량을 시스템에 등록(입고)하는 단계입니다. **지자체 관리자** 이상이 사용합니다.
|
봉투를 제작업체에 **주문(발주)** 하고, 도착한 봉투를 창고에 **들여놓는(입고)** 단계입니다.
|
||||||
|
|
||||||
## 발주
|
---
|
||||||
|
|
||||||
### 발주 등록
|
## 발주 등록 · *발주 입고 관리 › 발주 등록*
|
||||||
|
|
||||||
**발주 입고 관리 › 발주 등록**
|
봉투를 **얼마나 주문할지** 입력해 발주서를 만드는 화면입니다. 저장하면 추적용 **LOT 번호**가 자동으로 붙습니다.
|
||||||
|
|
||||||
1. 봉투 **품목**(종류·용량)과 **수량**, 납품 관련 정보를 입력합니다.
|
**이 화면의 용어**
|
||||||
2. 박스/낱장 수량과 금액·총계가 자동으로 계산됩니다.
|
- **발주가능봉투**: 조달청(나라장터)에 등록되어 주문할 수 있는 봉투 종류.
|
||||||
3. 저장하면 발주 건이 생성되고, 추적용 **LOT 번호**가 자동 부여됩니다.
|
- **입고처**: 들어온 봉투를 받을 창고/장소.
|
||||||
|
- **조달수수료**: 발주 금액에 붙는 수수료율(%).
|
||||||
|
- **Box당 팩 / 팩당 낱장 / 1박스 총 낱장**: 포장 환산 정보(참고용 표).
|
||||||
|
|
||||||
> 발주 내용은 무결성 보호를 위해 버전·해시로 관리됩니다. 수정(재발주) 시 기존 LOT는 유지됩니다.
|
**입력 항목**: 발주월, 발주일, 협회, 제작업체, 입고처, **봉투 품목별 수량(박스 단위)**.
|
||||||
|
|
||||||
### 발주 변경 · 현황
|
**버튼**: `발주`(저장) · `변경 저장`(수정 시) · `취소`.
|
||||||
|
|
||||||
| 작업 | 메뉴 | 설명 |
|
**작업 순서**
|
||||||
|---|---|---|
|
1. 발주월·발주일, 제작업체·입고처를 고릅니다.
|
||||||
| 발주 변경 | 발주 변경 | 기존 발주 수정·재발주 |
|
2. 아래 봉투 종류별로 **주문할 박스 수량**을 입력합니다(금액·총 낱장은 자동 계산).
|
||||||
| 발주 현황 | 발주 현황 | 발주 목록을 기간·상태로 조회, 엑셀 내보내기 |
|
3. `발주` 를 누르면 발주가 생성되고 LOT 번호가 부여됩니다.
|
||||||
| 발주 상세 | (현황에서 행 선택) | 개별 발주 상세 확인, 취소 처리 |
|
|
||||||
|
|
||||||
## 입고
|
---
|
||||||
|
|
||||||
발주분이 실제 도착하면 입고로 등록합니다. 입고 시 **박스·팩·낱장 바코드**가 생성되어 재고에 반영됩니다.
|
## 발주 현황 · *발주 입고 관리 › 발주 현황*
|
||||||
|
|
||||||
| 방식 | 메뉴 | 언제 사용 |
|
낸 발주를 **조회·관리**하는 목록 화면입니다.
|
||||||
|---|---|---|
|
|
||||||
| 스캐너 입고 | 발주 입고[스캐너] | 바코드를 스캔하며 입고 |
|
|
||||||
| 일괄 입고 | 일괄입고 | 다량을 한 번에 입고 |
|
|
||||||
| 입고 현황 | 입고 현황 | 입고 기록 조회, 엑셀 내보내기 |
|
|
||||||
|
|
||||||
### 입고 처리 순서
|
**필터**: 발주기간(월~월) · 제작업체 · 품명 · **입고구분(전체/입고완료/미입고)**.
|
||||||
|
|
||||||
1. 입고할 발주 건(LOT)을 선택합니다.
|
**표 컬럼**: 발주일자 · 제작업체 · 품명 · **발주수량 · 입고수량 · 미입고수량** · 발주금액 · 입고처 · 비고.
|
||||||
2. 도착 수량(박스/낱장)을 확인·입력합니다.
|
- **미입고수량** = 발주했지만 아직 안 들어온 수량.
|
||||||
3. 저장하면 재고가 증가하고, 단위별 바코드가 부여됩니다.
|
|
||||||
|
|
||||||
> 입고가 끝나면 **재고 관리**에서 수량이 정상 반영됐는지 확인하세요.
|
**버튼**: `엑셀저장` · `인쇄` · `발주등록`. (목록에서 개별 발주의 상세·취소 가능)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 입고 처리 · *발주 입고 관리 › 입고[스캐너] / 일괄입고*
|
||||||
|
|
||||||
|
도착한 봉투를 시스템에 **들여놓는** 화면입니다. 입고하면 박스·팩·낱장 **바코드가 생성**되고 재고가 늘어납니다.
|
||||||
|
|
||||||
|
**이 화면의 용어**
|
||||||
|
- **인계자(제작업체)** / **인수자(대행소)**: 봉투를 넘기는 쪽 / 받는 쪽.
|
||||||
|
- **입고량(매)**: 실제로 들어온 **낱장 수**("매" = 장).
|
||||||
|
- **LOT NO / 발주 NO**: 어떤 발주분인지 식별하는 번호.
|
||||||
|
|
||||||
|
**입고[스캐너]**: 발주 건을 보고 행마다 **입고량(매)** 을 직접 입력 → `입고처리`.
|
||||||
|
**일괄입고**: 여러 발주 건을 **체크박스로 골라** 한 번에 입고. 미입고량은 파란색으로 강조됩니다.
|
||||||
|
|
||||||
|
**작업 순서**
|
||||||
|
1. 제작업체·인수자·인계자·입고일을 고릅니다.
|
||||||
|
2. 들어온 만큼 **입고량(매)** 을 입력(또는 일괄 선택)합니다.
|
||||||
|
3. `입고처리` → 재고 반영. **재고 관리**에서 수량이 늘었는지 확인하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 입고 현황 · *발주 입고 관리 › 입고 현황*
|
||||||
|
|
||||||
|
입고 기록을 기간별로 조회합니다.
|
||||||
|
|
||||||
|
**필터**: 입고기간 · 제작업체 · 품명 · 입고구분(전체/완료/미완료).
|
||||||
|
**표 컬럼**: 입고일자 · 품명 · 입고수량 · 발주일자 · 발주수량 · 발주번호 · 제작업체 · **입고여부(완료/미완료)** · 입고처 · 비고.
|
||||||
|
**버튼**: `엑셀저장` · `인쇄`.
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
# 재고 · 실사
|
# 재고 · 실사
|
||||||
|
|
||||||
현재 보유 봉투 수량을 확인하고, 정기적으로 실사를 통해 실제 수량과 맞추는 단계입니다.
|
지금 창고에 **남은 봉투**를 확인하고, 컴퓨터 기록과 **실제 수량을 맞추는(실사)** 단계입니다.
|
||||||
|
|
||||||
## 재고 현황
|
---
|
||||||
|
|
||||||
**재고 관리 › 재고 현황**
|
## 재고 현황 · *재고 관리 › 재고 현황*
|
||||||
|
|
||||||
- 품목별·상태별 현재 재고를 조회합니다.
|
품목별로 **현재 남은 수량**을 봅니다.
|
||||||
- 지자체·봉투 종류 등으로 필터링할 수 있습니다.
|
|
||||||
- **엑셀 내보내기**로 목록을 저장할 수 있습니다.
|
|
||||||
|
|
||||||
| 항목 | 설명 |
|
**이 화면의 용어**
|
||||||
|---|---|
|
- **시군구재고**: 지자체(시·군·구) **창고**에 있는 재고.
|
||||||
| 품목 | 봉투 종류·용량 |
|
- **대행소재고**: 배송 **대행소**가 보유 중인 재고.
|
||||||
| 재고 수량 | 입고 − (판매 + 불출 + 파기) |
|
- **계**: 둘을 합친 총 재고.
|
||||||
| 상태 | 재고/판매 등 단위별 상태 |
|
|
||||||
|
|
||||||
## 실사 (재고 조사)
|
**필터**: 기준일자 · 대행소(전체/선택).
|
||||||
|
**표 컬럼**: 품목구분 · 봉투/스티커종류 · **계 · 시군구재고 · 대행소재고**.
|
||||||
|
**버튼**: `조회` · `엑셀저장` · `인쇄` · `실사선별조회`.
|
||||||
|
|
||||||
장부상 재고와 실제 창고 수량을 맞추는 작업입니다. 다음 순서로 진행합니다.
|
---
|
||||||
|
|
||||||
```
|
## 실사 (재고 확인) · *재고 관리 › 실사 선별 조회 / 실사 선별 관리*
|
||||||
실사 선별 ─→ 실사 등록(작업) ─→ 적용
|
|
||||||
```
|
|
||||||
|
|
||||||
| 단계 | 메뉴 | 하는 일 |
|
**실사**는 시스템에 적힌 수량(전산재고)과 **창고에서 직접 센 수량(실사재고)** 을 비교해 차이를 바로잡는 작업입니다.
|
||||||
|---|---|---|
|
|
||||||
| ① 선별 | 실사 선별 조회 | 실사 대상 범위를 골라 선별 |
|
|
||||||
| ② 작업 | 실사 선별 관리 | 실제 수량을 입력·기록 |
|
|
||||||
| ③ 상세·적용 | 실사 상세 | 실사 결과를 확인하고 재고에 **적용** |
|
|
||||||
|
|
||||||
### 실사 진행 순서
|
**이 화면의 용어**
|
||||||
|
- **전산재고**: 시스템 기록상 수량.
|
||||||
|
- **실사재고**: 현장에서 직접 센 수량(직접 입력).
|
||||||
|
- **차이**: 실사 − 전산. (양수 = 더 많음, 음수 = 부족)
|
||||||
|
- **박스 / 팩 / 낱장**: 셀 단위. 팩코드·낱장(시작~끝) 구간으로 표시됩니다.
|
||||||
|
|
||||||
1. **선별**: 실사할 품목·구간을 선택해 실사 건을 만듭니다.
|
**작업 순서**
|
||||||
2. **등록**: 실제 센 수량을 입력합니다. 장부 수량과의 차이가 표시됩니다.
|
1. **실사 선별**: 실사할 기간·품목을 골라 대상 목록을 만듭니다(팝업에서 작업일자·품목 선택).
|
||||||
3. **적용**: 검토 후 적용하면 차이가 재고에 반영됩니다.
|
2. **실사재고 입력**: 팩/박스별로 실제 센 수량을 입력하면 **차이**가 자동 표시됩니다.
|
||||||
|
3. **저장(적용)**: 검토 후 적용하면 차이가 재고에 반영됩니다.
|
||||||
|
|
||||||
> 적용 전까지는 재고에 영향을 주지 않으므로, 입력 도중 중단해도 안전합니다.
|
**주요 표 컬럼**: 팩코드 · 포장량 · 재고(전산) · **실사재고(입력)** · 차이 · 낱장(시작) · 낱장(끝).
|
||||||
|
|
||||||
|
> 적용 전까지는 재고에 영향을 주지 않으므로, 세는 도중 중단해도 안전합니다.
|
||||||
|
|||||||
@@ -1,46 +1,83 @@
|
|||||||
# 판매 · 불출
|
# 판매 · 반품 · 불출 · 주문
|
||||||
|
|
||||||
재고를 외부로 내보내는 두 가지 경로입니다. **판매**는 지정판매소 대상 유상 거래, **불출**은 무료 대상자에 대한 무상 지급입니다.
|
재고를 외부로 내보내는 단계입니다. **판매**(가게에 유상 공급)·**불출**(무료 배부)·**주문 접수**(전화 등).
|
||||||
|
|
||||||
## 판매 (지정판매소)
|
---
|
||||||
|
|
||||||
**판매 관리** 메뉴에서 처리합니다.
|
## 지정판매소 판매 · *판매 관리 › 지정 판매소 판매*
|
||||||
|
|
||||||
| 작업 | 메뉴 | 설명 |
|
동네 가게(지정판매소)에 봉투를 **판매**하고, 어떤 봉투를 줬는지 **바코드로 기록**합니다.
|
||||||
|---|---|---|
|
|
||||||
| 판매 등록 | 지정 판매소 판매 | 판매소를 선택해 판매 거래 기록 |
|
|
||||||
| 스캔 판매 | 지정 판매소 판매[스캔] | 바코드를 스캔하며 판매(판매소 현장용) |
|
|
||||||
| 판매 취소 | 지정 판매소 판매 취소 | 기존 판매 거래 취소 |
|
|
||||||
| 반품 | 지정 판매소 반품 | 판매분 반품 등록 |
|
|
||||||
| 반품 취소 | 지정 판매소 반품 취소 | 반품 취소 처리 |
|
|
||||||
|
|
||||||
### 판매 등록 순서
|
**이 화면의 용어**
|
||||||
|
- **판매소코드/상호/대표자**: 판매하는 가게 정보(검색해서 선택).
|
||||||
|
- **봉투코드(스캔)**: 내보내는 봉투의 바코드. 스캔/입력하면 어떤 LOT·포장단위인지 식별됩니다.
|
||||||
|
- **포장단위(Box/Pack/Sheet)**: 박스/팩/낱장.
|
||||||
|
|
||||||
1. 판매할 **지정판매소**를 선택합니다.
|
**입력/순서**
|
||||||
2. 봉투 **품목·수량**을 입력(또는 바코드 스캔)합니다.
|
1. 위에서 **판매소를 검색·선택**합니다(코드·상호·전화·주소로 검색).
|
||||||
3. 저장하면 재고가 감소하고 판매 내역이 기록됩니다.
|
2. 판매할 봉투 종류·수량을 고르거나 **봉투코드를 스캔**합니다.
|
||||||
|
3. `판매저장` → 재고가 줄고 판매 내역이 기록됩니다.
|
||||||
|
|
||||||
### 전화 접수(주문)
|
**표 컬럼**: (판매내역) 봉투종류·접수량·판매량·단가·판매금액 / (상세) 봉투종류·봉투코드·수량·포장단위.
|
||||||
|
|
||||||
| 작업 | 메뉴 |
|
---
|
||||||
|---|---|
|
|
||||||
| 전화 접수(신규) | 전화 접수 |
|
|
||||||
| 전화 접수 관리 | 전화 접수 관리(수정·취소) |
|
|
||||||
|
|
||||||
## 불출 (무료 대상자)
|
## 지정 판매소 반품 / 판매·반품 취소 · *판매 관리*
|
||||||
|
|
||||||
**불출 관리** 메뉴에서 무료 대상자에게 봉투를 무상 지급합니다.
|
- **반품**: 가게가 안 팔린 봉투를 **되돌려 받는** 것. 스캔/선택 후 저장하면 재고가 다시 늘어납니다.
|
||||||
|
- **판매 취소 / 반품 취소**: 잘못 처리한 건을 되돌립니다.
|
||||||
|
|
||||||
| 작업 | 메뉴 | 설명 |
|
---
|
||||||
|---|---|---|
|
|
||||||
| 불출 처리 | 무료용 불출 처리 | 무료 배분 등록(재고 감소) |
|
|
||||||
| 불출 취소 | 무료용 불출 취소 | 불출 취소(재고 복원) |
|
|
||||||
| 불출 현황 | 무료 불출 현황 | 기간별 불출 기록 조회 |
|
|
||||||
|
|
||||||
### 불출 처리 순서
|
## 판매/반품 현황 · *판매 관리* 또는 *판매 현황*
|
||||||
|
|
||||||
1. 무료 **대상처**와 봉투 **품목·수량**을 선택합니다.
|
기간별 판매·반품 내역을 봅니다.
|
||||||
2. 저장하면 재고가 감소하고 불출 내역이 기록됩니다.
|
|
||||||
3. 잘못 처리한 경우 **무료용 불출 취소**로 되돌리면 재고가 복원됩니다.
|
|
||||||
|
|
||||||
> 판매·불출 모두 재고를 감소시키므로, 처리 후 **재고 현황**에서 반영 결과를 확인하세요.
|
**필터**: 조회기간.
|
||||||
|
**표 컬럼**: 판매소 · 판매일 · 봉투코드 · 봉투명 · 수량 · 단가 · 금액 · **구분(판매/반품/취소)**.
|
||||||
|
**버튼**: `조회` · `초기화` · `주문등록` · `판매등록`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 전화 주문 접수 · *판매 관리 › 전화 접수*
|
||||||
|
|
||||||
|
가게가 전화로 주문한 내용을 **접수**합니다(실제 출고/판매는 이후 단계).
|
||||||
|
|
||||||
|
**이 화면의 용어**
|
||||||
|
- **접수일 / 배달일**: 주문 받은 날 / 가져다줄 날(보통 다음날 자동).
|
||||||
|
- **결제구분**: 이체 / 가상계좌.
|
||||||
|
- **1박스·1팩(낱장/판매가)**: 포장별 수량·가격 참고값.
|
||||||
|
|
||||||
|
**입력/순서**
|
||||||
|
1. **판매소 검색·선택**(코드·사업자번호·상호·전화·주소).
|
||||||
|
2. 결제구분을 고르고, 봉투 **품목·주문수량·포장단위(박스/팩/낱장)** 를 입력(`행추가`로 여러 품목).
|
||||||
|
3. `등록` → 주문 접수 완료.
|
||||||
|
|
||||||
|
> **주문 접수(간편)**: 판매소·배달일·결제방법과 봉투별 수량만 입력하는 간단 버전.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 무료용 불출 처리 · *불출 관리 › 무료용 불출 처리*
|
||||||
|
|
||||||
|
무료 대상자(동사무소 등)에게 봉투를 **무상으로 내보내는(불출)** 화면입니다.
|
||||||
|
|
||||||
|
**이 화면의 용어**
|
||||||
|
- **불출구분(무료용/공공용)**: 주민 무료 배부용 / 공공기관용.
|
||||||
|
- **불출처**: 봉투를 최종 전달할 곳(동사무소·구청·기타).
|
||||||
|
- **재고(낱장) / 환산(낱장)**: 현재 재고 / 입력 수량을 낱장으로 환산한 값.
|
||||||
|
|
||||||
|
**입력/순서**
|
||||||
|
1. 불출년도·분기, 불출구분, 불출일, **불출처(동)** 를 고릅니다.
|
||||||
|
2. **바코드 스캔** 또는 `행추가`로 봉투 종류·수량·포장단위를 입력합니다.
|
||||||
|
3. `저장` → 재고가 줄고 불출 내역이 기록됩니다.
|
||||||
|
|
||||||
|
**표 컬럼**: 봉투코드 · 봉투종류 · 수량 · 포장 · 재고(낱장) · 환산(낱장).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 무료용 불출 취소 · *불출 관리 › 무료용 불출 취소*
|
||||||
|
|
||||||
|
잘못 불출한 건을 **되돌려 재고를 복원**합니다.
|
||||||
|
|
||||||
|
**필터**: 불출월 · 불출처 · 불출구분 · 봉투종류.
|
||||||
|
**순서**: 불출 목록에서 건을 고르고, 품목 내역에서 **취소할 항목을 체크 → 취소수량 입력** 후 처리.
|
||||||
|
|||||||
@@ -1,42 +1,63 @@
|
|||||||
# 판매현황 · 수불 · 통계
|
# 현황 · 리포트 · 수불
|
||||||
|
|
||||||
판매·재고 흐름을 집계해 보여주는 조회·리포트 화면 모음입니다. 대부분 **기간을 지정해 조회**하고 인쇄·엑셀로 내보낼 수 있습니다.
|
입고·판매·불출 기록을 **모아 보여주는** 조회 화면들입니다. 대부분 **기간을 지정해 조회**하고 `엑셀저장`·`인쇄`로 내보낼 수 있습니다.
|
||||||
|
|
||||||
## 판매 현황
|
---
|
||||||
|
|
||||||
| 메뉴 | 내용 |
|
## 기간별 봉투 수불 현황 · *봉투 수불 관리 › 기간별 봉투 수불 현황*
|
||||||
|---|---|
|
|
||||||
| 지정 판매소 일/기간 판매대장 | 판매소별 일자·기간 거래 장부 |
|
|
||||||
| 일계표 | 하루 전체 판매 요약(일계 + 월 누계) |
|
|
||||||
| 기간별 판매현황 | 지정 기간의 판매 집계(일집계/기간집계) |
|
|
||||||
| 년 판매 현황 | 연간 판매 통계(월별/분기별) |
|
|
||||||
| 지정 판매소별 판매현황 | 판매소별 수량·금액 비교 |
|
|
||||||
| 홈텍스 처리 | 세금계산서용 데이터(엑셀) 생성 |
|
|
||||||
|
|
||||||
## 봉투 수불 관리
|
**수불(受拂)** = 들어오고 나간 움직임. 기간 동안 봉투가 얼마나 들어오고(입고) 나갔는지(판매·불출 등)를 한 표로 봅니다.
|
||||||
|
|
||||||
입고·판매·불출·반품·파기를 한데 모아 **수불(수입·불출)** 흐름을 봅니다.
|
**이 화면의 용어**
|
||||||
|
- **전일재고**: 조회 시작일 **전날**의 재고.
|
||||||
|
- **입고**: 입고량 + 반품 + 기타.
|
||||||
|
- **출고**: 판매 + 무료불출 + 반품 + 기타.
|
||||||
|
- **잔량**: 전일재고 + 입고 − 출고.
|
||||||
|
|
||||||
| 메뉴 | 내용 |
|
**필터**: 조회기간 · 봉투형식 · 봉투구분 · 대행소 · **집계방식(일자별/기간별)**.
|
||||||
|---|---|
|
**표 컬럼**: 일자 · 품목 · 전일재고 · 입고(소계) · 출고(소계) · 잔량.
|
||||||
| 기간별 봉투 수불 현황 | 기간 내 재고/입고/판매/불출 종합(엑셀 가능) |
|
|
||||||
| 기타 입출고 | 손상·기증·폐기 등 기타 입출고 등록·조회 |
|
|
||||||
| 반품/파기 현황 | 반품 및 파기 내역 |
|
|
||||||
| LOT 수불 조회 | 봉투번호(바코드)·LOT 단위 이력 추적 |
|
|
||||||
| 쓰레기 봉투 수급 계획 | 공급·수요 계획 |
|
|
||||||
|
|
||||||
### LOT 수불 조회 사용법
|
---
|
||||||
|
|
||||||
1. **봉투번호(바코드)** 또는 LOT 번호를 입력합니다.
|
## 일계표 · *판매 현황 › 일계표*
|
||||||
2. 조회하면 해당 단위의 입고·판매·반품 이력이 표시됩니다.
|
|
||||||
3. 입력할 코드 형식이 헷갈리면 **도움말 › 번호알기**로 먼저 확인하세요.
|
|
||||||
|
|
||||||
## 통계 분석
|
하루치 판매를 **일계(당일)** 와 **누계(월 누적)** 로 집계합니다.
|
||||||
|
|
||||||
| 메뉴 | 내용 |
|
**용어**: **일계** = 그날 합계, **누계(월)** = 월초~당일 누적, **징수액** = 판매금액 − 수수료.
|
||||||
|---|---|
|
**필터**: 조회일자 · 대행소 · 구분.
|
||||||
| 전년 대비 판매 분석 | 작년 동기 대비 비교(차트) |
|
**표**: 봉투종류별 — 일계(수량·판매금액·수수료·징수액) / 누계(월) 동일 항목.
|
||||||
| 월별 판매 추이 분석 | 월별 추이 시각화 |
|
|
||||||
| 계절별 판매 추이 분석 | 계절 패턴 분석 |
|
|
||||||
|
|
||||||
> 리포트 화면은 조회 조건(기간·품목·판매소)을 바꿔가며 반복 조회할 수 있습니다. 결과는 인쇄 버튼 또는 엑셀 내보내기로 저장하세요.
|
---
|
||||||
|
|
||||||
|
## 지정 판매소별 판매현황 · *판매 현황 › 판매소별 판매현황*
|
||||||
|
|
||||||
|
판매소마다 **얼마나 팔았는지**(수량 또는 금액)를 월별로 비교합니다.
|
||||||
|
|
||||||
|
**필터**: 기간 · 읍면동 · 봉투종류 · **지표(수량/금액)**.
|
||||||
|
**표**: 판매소명 · 판매소코드 · 월별 값 · 합계.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LOT 수불 조회 · *봉투 수불 관리 › LOT 수불 조회*
|
||||||
|
|
||||||
|
특정 **봉투번호(바코드)** 또는 **LOT**의 입고·판매·반품 **이력**을 추적합니다.
|
||||||
|
|
||||||
|
**입력**: 봉투번호(바코드/팩코드/박스코드/낱장코드).
|
||||||
|
**표**: 일자 · 품목 · 포장단위 · **구분(입고/판매/반품)** · 수량 · LOT번호.
|
||||||
|
> 입력할 코드 형식이 헷갈리면 좌측 **[봉투·LOT·바코드 코드체계]** 또는 도움말의 **번호알기**를 참고하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 반품/파기 현황 · *봉투 수불 관리 › 반품/파기 현황*
|
||||||
|
|
||||||
|
**용어**: **반품** = 판매소가 되돌린 봉투(출고 탭) / **파기** = 반품분의 폐기(입고 탭).
|
||||||
|
**필터**: 조회기간 · 입출고구분.
|
||||||
|
**표**: 일자 · 판매소명 · 봉투종류 · 수량 · 구분(반품/파기).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 그 밖의 현황
|
||||||
|
- **기간별 판매현황 / 년 판매 현황**: 기간·연도 단위 판매 집계.
|
||||||
|
- **지정 판매소 (일/기간) 판매대장**: 판매소별 거래 장부.
|
||||||
|
- **홈택스 처리**: 세금계산서용 데이터(엑셀) 생성.
|
||||||
|
- **통계 분석(전년대비·월별·계절 추이)**: 판매 추세를 그래프로.
|
||||||
|
|||||||
69
app/Docs/manual/60_basic_info.md
Normal file
69
app/Docs/manual/60_basic_info.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# 기본정보 (판매소 · 단가 · 코드)
|
||||||
|
|
||||||
|
업무의 **기준이 되는 정보**를 관리하는 화면들입니다. 발주·판매가 이 값을 사용하므로 먼저 정확히 등록되어 있어야 합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 지정판매소 관리 · *기본정보관리 › 지정 판매소 관리/조회*
|
||||||
|
|
||||||
|
봉투를 파는 **가게(지정판매소)** 를 등록·조회합니다.
|
||||||
|
|
||||||
|
**이 화면의 용어**
|
||||||
|
- **판매소번호**: 가게 고유 번호(지역코드 + 일련번호).
|
||||||
|
- **도로명주소 / 지번주소**: 두 가지 주소 체계(지도 표시·검색에 사용).
|
||||||
|
- **은행/계좌, 가상계좌**: 봉투 대금 결제용 계좌.
|
||||||
|
|
||||||
|
**목록 표 컬럼**: 번호 · 판매소번호 · 상호명 · 대표자명 · 지역/읍면동 · 전화번호 · 주소.
|
||||||
|
**상세**: 사업자번호 · 우편번호 · 도로명/지번주소 · 이메일 · 결제 계좌 등.
|
||||||
|
> 목록에서 가게를 고르면 우측에 상세가 표시됩니다. (등록·수정은 관리자)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 단가 관리 · *기본정보관리 › 단가 관리*
|
||||||
|
|
||||||
|
봉투 **가격**을 기간별로 관리합니다.
|
||||||
|
|
||||||
|
**이 화면의 용어**
|
||||||
|
- **발주단가**: 제작업체에 주는 가격(살 때).
|
||||||
|
- **도매단가**: 대행소·판매소에 넘기는 도매 가격.
|
||||||
|
- **판매단가**: 최종 소비자 판매가.
|
||||||
|
- **수수료율**: 판매수수료율(%).
|
||||||
|
- **적용시작/종료**: 그 단가가 유효한 기간.
|
||||||
|
|
||||||
|
**필터**: 봉투구분 · 봉투코드 · 조회기간.
|
||||||
|
**표 컬럼**: 봉투코드 · 봉투명 · 발주단가 · 도매단가 · 판매단가 · 수수료율 · 적용시작 · 적용종료 · 상태.
|
||||||
|
> 조회 전용이며, 등록·수정은 `단가관리(CRUD)` 화면에서 합니다(이력 보존).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 포장 단위 관리 · *기본정보관리 › 포장 단위 관리*
|
||||||
|
|
||||||
|
봉투 1박스·1팩에 **몇 장이 들어가는지** 정의합니다. 이 값으로 박스↔낱장이 환산됩니다.
|
||||||
|
|
||||||
|
**이 화면의 용어**
|
||||||
|
- **박스당 팩수**: 1박스 안의 팩 개수.
|
||||||
|
- **팩당 낱장수**: 1팩 안의 낱장(봉투) 수.
|
||||||
|
- **1박스 총 낱장** = 박스당 팩수 × 팩당 낱장수.
|
||||||
|
|
||||||
|
**표 컬럼**: 봉투코드 · 봉투명 · 박스당팩수 · 팩당낱장수 · 1박스총낱장 · 적용시작/종료 · 상태(사용/만료).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 기본코드 관리 · *기본정보관리 › 기본 코드 관리*
|
||||||
|
|
||||||
|
시스템 곳곳의 **선택 항목(드롭다운)** 값을 관리합니다. 왼쪽에 **코드 종류**, 오른쪽에 그 종류의 **세부코드**가 나옵니다.
|
||||||
|
|
||||||
|
**이 화면의 용어**
|
||||||
|
- **코드 종류**: 분류(예: 봉투구분, 동코드, 결제구분, 불출구분).
|
||||||
|
- **세부코드**: 그 분류의 실제 값(예: 봉투구분 → 봉투/스티커).
|
||||||
|
|
||||||
|
**자주 쓰는 코드 종류**
|
||||||
|
- **봉투구분**(봉투/스티커) · **동코드**(지역 동) · **결제구분**(이체/가상계좌) · **불출구분**(무료용/공공용).
|
||||||
|
|
||||||
|
**표 컬럼**: 코드 · 코드명 · (세부코드 개수) · 상태(사용/미사용) · 작업(수정/삭제 — 관리자).
|
||||||
|
> 등록·수정·삭제는 슈퍼 관리자만 가능합니다. 조회는 누구나 가능합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 그 밖의 기본정보
|
||||||
|
- **판매 대행소 / 담당자 / 업체(제작·협회·회수) / 무료용 대상자 관리**: 각각 거래처·담당자·대상처를 등록·조회하는 목록 화면입니다(등록·수정은 관리자).
|
||||||
@@ -936,3 +936,32 @@ if (! function_exists('gov_portal_nav_partial_vars')) {
|
|||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('manual_help_url_for_path')) {
|
||||||
|
/**
|
||||||
|
* 현재(또는 주어진) 화면 경로에 대응하는 매뉴얼 URL. 매칭 없으면 ''.
|
||||||
|
* Config\Manual::$screenHelp 의 가장 긴(구체적) 접두를 우선 매칭한다.
|
||||||
|
*/
|
||||||
|
function manual_help_url_for_path(?string $path = null): string
|
||||||
|
{
|
||||||
|
helper('url');
|
||||||
|
$path = strtolower(trim($path ?? current_nav_request_path(), '/'));
|
||||||
|
if ($path === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$map = config(\Config\Manual::class)->screenHelp ?? [];
|
||||||
|
$bestSlug = '';
|
||||||
|
$bestLen = -1;
|
||||||
|
foreach ($map as $prefix => $slug) {
|
||||||
|
$p = strtolower((string) $prefix);
|
||||||
|
if ($path === $p || str_starts_with($path . '/', $p . '/')) {
|
||||||
|
if (strlen($p) > $bestLen) {
|
||||||
|
$bestLen = strlen($p);
|
||||||
|
$bestSlug = (string) $slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bestSlug !== '' ? base_url('bag/manual/' . $bestSlug) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -202,6 +202,8 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
|
|||||||
// 뒤로가기(bfcache 복원)·탭 복귀 시에도 최근 목록 갱신
|
// 뒤로가기(bfcache 복원)·탭 복귀 시에도 최근 목록 갱신
|
||||||
window.addEventListener('pageshow', renderRecent);
|
window.addEventListener('pageshow', renderRecent);
|
||||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) renderRecent(); });
|
document.addEventListener('visibilitychange', function () { if (!document.hidden) renderRecent(); });
|
||||||
|
// 다른 탭(iframe)에서 메뉴를 방문해 기록이 바뀌면 즉시 반영
|
||||||
|
window.addEventListener('storage', function (e) { if (e.key === 'jrj_recent_menus') renderRecent(); });
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ declare(strict_types=1);
|
|||||||
* @var bool $bare true면 본문을 카드 래퍼 없이 그대로 출력(대시보드용)
|
* @var bool $bare true면 본문을 카드 래퍼 없이 그대로 출력(대시보드용)
|
||||||
*/
|
*/
|
||||||
$bare = ! empty($bare);
|
$bare = ! empty($bare);
|
||||||
|
helper('admin');
|
||||||
|
$helpUrl = function_exists('manual_help_url_for_path') ? manual_help_url_for_path() : '';
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
@@ -50,8 +52,15 @@ tailwind.config = {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div style="padding: 0.875rem 1rem 1.25rem;">
|
<div style="padding: 0.875rem 1rem 1.25rem;">
|
||||||
<?php if (! empty($title)): ?>
|
<?php if (! empty($title) || $helpUrl !== ''): ?>
|
||||||
<h1 class="embed-titlebar"><i class="fa-solid fa-folder-open" style="color:#007bff;"></i><?= esc($title) ?></h1>
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin:0 0 .75rem;">
|
||||||
|
<h1 class="embed-titlebar" style="margin:0;"><?php if (! empty($title)): ?><i class="fa-solid fa-folder-open" style="color:#007bff;"></i><?= esc($title) ?><?php endif; ?></h1>
|
||||||
|
<?php if ($helpUrl !== ''): ?>
|
||||||
|
<a href="<?= esc($helpUrl, 'attr') ?>" class="embed-help no-print" style="display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .6rem;border-radius:6px;background:#eef2f7;border:1px solid #dde4ec;color:#1a2b4b;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap;">
|
||||||
|
<i class="fa-regular fa-circle-question"></i> 이 화면 설명
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (session()->getFlashdata('success')): ?>
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
<div class="embed-flash ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
<div class="embed-flash ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||||
@@ -73,6 +82,61 @@ tailwind.config = {
|
|||||||
if (window.top !== window.self && /\/login(\/|$)/.test(location.pathname)) {
|
if (window.top !== window.self && /\/login(\/|$)/.test(location.pathname)) {
|
||||||
try { window.top.location.href = location.href; } catch (e) {}
|
try { window.top.location.href = location.href; } catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 방문한 업무 메뉴 경로 기록 (대시보드 "최근 방문 메뉴"용). localStorage 는 동일 출처 탭과 공유된다.
|
||||||
|
try {
|
||||||
|
var rp = (location.pathname || '').replace(/\/+$/, '') || '/';
|
||||||
|
if (rp !== '/' && !/\/login|\/logout|\/register|\/manual/.test(rp)) {
|
||||||
|
var RK = 'jrj_recent_menus';
|
||||||
|
var ra = JSON.parse(localStorage.getItem(RK) || '[]');
|
||||||
|
if (!Array.isArray(ra)) ra = [];
|
||||||
|
ra = ra.filter(function (x) { return x && x.p && x.p !== rp; });
|
||||||
|
ra.unshift({ p: rp, t: Date.now() });
|
||||||
|
localStorage.setItem(RK, JSON.stringify(ra.slice(0, 12)));
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 탭(iframe) 안에서의 링크·폼 이동은 항상 embed 유지 → 중첩 헤더/사이드바 방지
|
||||||
|
function withEmbed(href) {
|
||||||
|
try {
|
||||||
|
var u = new URL(href, location.href);
|
||||||
|
if (u.origin !== location.origin) return null;
|
||||||
|
if (u.searchParams.get('embed') === '1') return null;
|
||||||
|
u.searchParams.set('embed', '1');
|
||||||
|
return u.href;
|
||||||
|
} catch (e) { return null; }
|
||||||
|
}
|
||||||
|
// "이 화면 설명" → 워크스페이스 새 탭으로 매뉴얼 열기(없으면 새 창)
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var h = e.target.closest ? e.target.closest('a.embed-help') : null;
|
||||||
|
if (!h) return;
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
var url = h.getAttribute('href');
|
||||||
|
try {
|
||||||
|
if (window.parent && window.parent !== window && typeof window.parent.wsOpenTab === 'function') {
|
||||||
|
window.parent.wsOpenTab(url, '도움말'); return;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var a = e.target.closest ? e.target.closest('a[href]') : null;
|
||||||
|
if (!a || a.classList.contains('embed-help')) return;
|
||||||
|
var t = (a.getAttribute('target') || '').toLowerCase();
|
||||||
|
if (t && t !== '_self') return;
|
||||||
|
if (a.hasAttribute('download')) return;
|
||||||
|
var href = a.getAttribute('href') || '';
|
||||||
|
if (!href || href.charAt(0) === '#' || /^(javascript:|mailto:|tel:|data:)/i.test(href)) return;
|
||||||
|
var ne = withEmbed(href);
|
||||||
|
if (ne) a.setAttribute('href', ne);
|
||||||
|
}, true);
|
||||||
|
document.addEventListener('submit', function (e) {
|
||||||
|
var f = e.target;
|
||||||
|
if (!f || f.tagName !== 'FORM') return;
|
||||||
|
var ne = withEmbed(f.getAttribute('action') || location.href);
|
||||||
|
if (ne) f.setAttribute('action', ne);
|
||||||
|
}, true);
|
||||||
// 표 '번호' 컬럼 역순 채번 (사이트 레이아웃과 동일)
|
// 표 '번호' 컬럼 역순 채번 (사이트 레이아웃과 동일)
|
||||||
var run = function () {
|
var run = function () {
|
||||||
document.querySelectorAll('table').forEach(function (table) {
|
document.querySelectorAll('table').forEach(function (table) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ if ($effectiveLgIdx) {
|
|||||||
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
|
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
|
||||||
$lgLabel = $lgRow ? (string) $lgRow->lg_name : '';
|
$lgLabel = $lgRow ? (string) $lgRow->lg_name : '';
|
||||||
}
|
}
|
||||||
|
$helpUrl = function_exists('manual_help_url_for_path') ? manual_help_url_for_path() : '';
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko" class="gov-portal-html">
|
<html lang="ko" class="gov-portal-html">
|
||||||
@@ -116,7 +117,14 @@ tailwind.config = {
|
|||||||
|
|
||||||
<main class="main work-main main-content-area">
|
<main class="main work-main main-content-area">
|
||||||
<?php if (! $bare && ! empty($title)): ?>
|
<?php if (! $bare && ! empty($title)): ?>
|
||||||
<h1 class="work-titlebar"><i class="fa-solid fa-folder-open tb-ico"></i><?= esc($title) ?></h1>
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;">
|
||||||
|
<h1 class="work-titlebar" style="margin-bottom:0;"><i class="fa-solid fa-folder-open tb-ico"></i><?= esc($title) ?></h1>
|
||||||
|
<?php if ($helpUrl !== ''): ?>
|
||||||
|
<a href="<?= esc($helpUrl, 'attr') ?>" target="_blank" rel="noopener" class="no-print" style="display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .6rem;border-radius:6px;background:#fff;border:1px solid var(--border);color:#1a2b4b;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap;">
|
||||||
|
<i class="fa-regular fa-circle-question"></i> 이 화면 설명
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (session()->getFlashdata('success')): ?>
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
<div class="work-flash ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
<div class="work-flash ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ if ($effectiveLgIdx) {
|
|||||||
.ws-tab { display: inline-flex; align-items: center; gap: .4rem; max-width: 200px; padding: .35rem .6rem; background: #f5f7fa; border: 1px solid var(--border); border-bottom: none; border-radius: 7px 7px 0 0; font-size: .78rem; color: #555; cursor: pointer; white-space: nowrap; }
|
.ws-tab { display: inline-flex; align-items: center; gap: .4rem; max-width: 200px; padding: .35rem .6rem; background: #f5f7fa; border: 1px solid var(--border); border-bottom: none; border-radius: 7px 7px 0 0; font-size: .78rem; color: #555; cursor: pointer; white-space: nowrap; }
|
||||||
.ws-tab .t-name { overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
|
.ws-tab .t-name { overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
|
||||||
.ws-tab.active { background: #fff; color: var(--navy); font-weight: 700; box-shadow: 0 -2px 0 #007bff inset; }
|
.ws-tab.active { background: #fff; color: var(--navy); font-weight: 700; box-shadow: 0 -2px 0 #007bff inset; }
|
||||||
.ws-tab .t-close { width: 16px; height: 16px; line-height: 14px; text-align: center; border-radius: 50%; color: #999; font-size: 12px; }
|
.ws-tab .t-refresh, .ws-tab .t-close { width: 16px; height: 16px; line-height: 14px; text-align: center; border-radius: 50%; color: #999; font-size: 12px; }
|
||||||
|
.ws-tab .t-refresh:hover { background: #dbeafe; color: #1d4ed8; }
|
||||||
.ws-tab .t-close:hover { background: #e2e8f0; color: #333; }
|
.ws-tab .t-close:hover { background: #e2e8f0; color: #333; }
|
||||||
.ws-panels { flex: 1; position: relative; min-height: 0; background: #f0f4f8; }
|
.ws-panels { flex: 1; position: relative; min-height: 0; background: #f0f4f8; }
|
||||||
.ws-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: none; background: #f0f4f8; }
|
.ws-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: none; background: #f0f4f8; }
|
||||||
@@ -114,6 +115,13 @@ if ($effectiveLgIdx) {
|
|||||||
if (empty) empty.style.display = 'none';
|
if (empty) empty.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reloadTab(id) {
|
||||||
|
var t = tabs[id];
|
||||||
|
if (!t) return;
|
||||||
|
try { t.frame.contentWindow.location.reload(); }
|
||||||
|
catch (e) { t.frame.src = t.frame.src; } // 교차출처 등 예외 시 src 재설정
|
||||||
|
}
|
||||||
|
|
||||||
function closeTab(id) {
|
function closeTab(id) {
|
||||||
if (!tabs[id]) return;
|
if (!tabs[id]) return;
|
||||||
tabs[id].frame.remove();
|
tabs[id].frame.remove();
|
||||||
@@ -142,11 +150,20 @@ if ($effectiveLgIdx) {
|
|||||||
var nameSpan = document.createElement('span');
|
var nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 't-name';
|
nameSpan.className = 't-name';
|
||||||
nameSpan.textContent = title || '탭';
|
nameSpan.textContent = title || '탭';
|
||||||
|
var refresh = document.createElement('span');
|
||||||
|
refresh.className = 't-refresh';
|
||||||
|
refresh.textContent = '↻';
|
||||||
|
refresh.title = '이 탭 새로고침';
|
||||||
var close = document.createElement('span');
|
var close = document.createElement('span');
|
||||||
close.className = 't-close';
|
close.className = 't-close';
|
||||||
close.textContent = '×';
|
close.textContent = '×';
|
||||||
btn.appendChild(nameSpan); btn.appendChild(close);
|
close.title = '탭 닫기';
|
||||||
btn.addEventListener('click', function (e) { if (e.target === close) { closeTab(id); } else { activate(id); } });
|
btn.appendChild(nameSpan); btn.appendChild(refresh); btn.appendChild(close);
|
||||||
|
btn.addEventListener('click', function (e) {
|
||||||
|
if (e.target === close) { closeTab(id); }
|
||||||
|
else if (e.target === refresh) { activate(id); reloadTab(id); }
|
||||||
|
else { activate(id); }
|
||||||
|
});
|
||||||
bar.appendChild(btn);
|
bar.appendChild(btn);
|
||||||
|
|
||||||
tabs[id] = { url: url, title: title, frame: frame, btn: btn };
|
tabs[id] = { url: url, title: title, frame: frame, btn: btn };
|
||||||
@@ -160,8 +177,8 @@ if ($effectiveLgIdx) {
|
|||||||
var a = e.target.closest('a[href]');
|
var a = e.target.closest('a[href]');
|
||||||
if (!a) return;
|
if (!a) return;
|
||||||
var href = a.getAttribute('href') || '';
|
var href = a.getAttribute('href') || '';
|
||||||
// 외부/특수 링크(로그아웃, 지자체선택 등 사이드바 하단 블록)는 그대로 이동
|
// 지자체 선택(sb-gray)·모바일앱(sb-teal)은 그대로 이동, 나머지(소메뉴·하단 링크)는 탭으로
|
||||||
if (a.closest('.sidebar-blocks')) return;
|
if (a.closest('.sb-gray') || a.closest('.sb-teal')) return;
|
||||||
if (/\/logout|\/login/.test(href) || href.charAt(0) === '#' || href === '') return;
|
if (/\/logout|\/login/.test(href) || href.charAt(0) === '#' || href === '') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openTab(href, (a.textContent || '').trim());
|
openTab(href, (a.textContent || '').trim());
|
||||||
@@ -177,6 +194,15 @@ if ($effectiveLgIdx) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 브라우저 전체 새로고침/이탈 시 경고 (기본 대시보드 외 탭이 열려 있을 때)
|
||||||
|
window.addEventListener('beforeunload', function (e) {
|
||||||
|
if (order.length > 1) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '열어 둔 탭이 모두 닫힙니다. 계속할까요?';
|
||||||
|
return e.returnValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 첫 화면: 대시보드 탭 자동 열기
|
// 첫 화면: 대시보드 탭 자동 열기
|
||||||
openTab('<?= base_url('/') ?>', '업무 현황');
|
openTab('<?= base_url('/') ?>', '업무 현황');
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ $activeChildHref = strtolower(ltrim((string) ($govActiveChildHref ?? ''), '/'));
|
|||||||
</div>
|
</div>
|
||||||
<div class="sb-links">
|
<div class="sb-links">
|
||||||
<a href="<?= base_url('bag/help') ?>">나의 할일</a>
|
<a href="<?= base_url('bag/help') ?>">나의 할일</a>
|
||||||
<a href="<?= base_url('dashboard') ?>">종합·그래프</a>
|
<a href="<?= base_url('bag/manual') ?>">사용자 매뉴얼</a>
|
||||||
<a href="<?= base_url('bag/help') ?>">FAQ</a>
|
<a href="<?= base_url('bag/help') ?>">FAQ</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
95
e2e/qa_sweep.spec.js
Normal file
95
e2e/qa_sweep.spec.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
const { login } = require('./helpers/auth');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 점검 스윕 — 주요 화면 콘솔에러·차단 오버레이 점검 + 매뉴얼/도움말/워크스페이스 통합 검증.
|
||||||
|
*/
|
||||||
|
async function selectDaegu(page) {
|
||||||
|
await page.goto('/admin/select-local-government');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const r = document.querySelector('input[name="lg_idx"][value="1"]');
|
||||||
|
if (r) { r.checked = true; r.form.submit(); }
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(700);
|
||||||
|
}
|
||||||
|
|
||||||
|
// kakao 외부 SDK 관련(도메인 미등록 환경) 잡음은 제외
|
||||||
|
function appError(msg) {
|
||||||
|
return !/kakao|dapi\.kakao|sdk\.js|OPEN_MAP_AND_LOCAL|appkey/i.test(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGES = [
|
||||||
|
'/', '/bag/inventory', '/bag/order/create', '/bag/bag-orders',
|
||||||
|
'/bag/receiving/scanner', '/bag/receiving/batch', '/bag/sale/designated',
|
||||||
|
'/bag/issue/create', '/bag/issue', '/bag/flow', '/bag/sales',
|
||||||
|
'/bag/reports/daily-summary', '/bag/reports/lot-flow', '/bag/reports/returns',
|
||||||
|
'/bag/bag-prices', '/bag/packaging-units', '/bag/code-kinds',
|
||||||
|
'/bag/designated-shops', '/bag/designated-shops/browse', '/bag/number-lookup',
|
||||||
|
'/bag/manual', '/admin', '/admin/menus', '/admin/users', '/admin/access/login-history',
|
||||||
|
];
|
||||||
|
|
||||||
|
test.describe('QA 스윕', () => {
|
||||||
|
test('주요 화면 콘솔 에러·차단 오버레이 점검', async ({ page }) => {
|
||||||
|
await login(page, 'admin');
|
||||||
|
await selectDaegu(page);
|
||||||
|
const problems = [];
|
||||||
|
for (const url of PAGES) {
|
||||||
|
const errs = [];
|
||||||
|
page.removeAllListeners('pageerror');
|
||||||
|
page.on('pageerror', (e) => { if (appError(String(e))) errs.push(String(e)); });
|
||||||
|
const res = await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForTimeout(700);
|
||||||
|
const status = res ? res.status() : 0;
|
||||||
|
const cover = await page.evaluate(() => {
|
||||||
|
const cx = Math.floor(innerWidth / 2), cy = Math.floor(innerHeight / 2);
|
||||||
|
let n = 0;
|
||||||
|
document.querySelectorAll('*').forEach((el) => {
|
||||||
|
const s = getComputedStyle(el);
|
||||||
|
if ((s.position === 'fixed' || s.position === 'absolute') && s.display !== 'none' &&
|
||||||
|
s.visibility !== 'hidden' && parseFloat(s.opacity || '1') > 0.1 && s.pointerEvents !== 'none') {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
if (r.width >= innerWidth * 0.85 && r.height >= innerHeight * 0.7 &&
|
||||||
|
!/portal-header|sidebar|ws-/.test(el.className || '')) n++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return n;
|
||||||
|
});
|
||||||
|
if (status >= 400) problems.push(`${url} → HTTP ${status}`);
|
||||||
|
if (errs.length) problems.push(`${url} → JS오류: ${errs[0]}`);
|
||||||
|
if (cover > 0) problems.push(`${url} → 화면 덮는 오버레이 ${cover}개`);
|
||||||
|
}
|
||||||
|
console.log('>>> SWEEP problems=' + JSON.stringify(problems, null, 0));
|
||||||
|
expect(problems, problems.join('\n')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('매뉴얼 전체 페이지 렌더', async ({ page }) => {
|
||||||
|
await login(page, 'user');
|
||||||
|
const slugs = ['overview', 'flow', 'order', 'inventory', 'sales', 'reports', 'basic', 'codes', 'faq'];
|
||||||
|
for (const s of slugs) {
|
||||||
|
const res = await page.goto('/bag/manual/' + s, { waitUntil: 'domcontentloaded' });
|
||||||
|
expect(res.status(), s).toBe(200);
|
||||||
|
await expect(page.locator('.manual-prose')).not.toBeEmpty();
|
||||||
|
}
|
||||||
|
// 미등록 slug → 404
|
||||||
|
const bad = await page.goto('/bag/manual/zzz-none', { waitUntil: 'domcontentloaded' });
|
||||||
|
expect(bad.status()).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('이 화면 설명 매핑 정확', async ({ page }) => {
|
||||||
|
await login(page, 'admin');
|
||||||
|
await selectDaegu(page);
|
||||||
|
const cases = [
|
||||||
|
['/bag/inventory', 'inventory'],
|
||||||
|
['/bag/order/create', 'order'],
|
||||||
|
['/bag/sale/designated', 'sales'],
|
||||||
|
['/bag/flow', 'reports'],
|
||||||
|
['/bag/bag-prices', 'basic'],
|
||||||
|
['/bag/number-lookup', 'codes'],
|
||||||
|
];
|
||||||
|
for (const [url, slug] of cases) {
|
||||||
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
|
const href = await page.locator('a.no-print', { hasText: '이 화면 설명' }).first().getAttribute('href');
|
||||||
|
expect(href, url).toContain('/bag/manual/' + slug);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user