환경 초기화 및 벤더 리포지토리 스켈레톤 도입
14
.env.development.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
API_BASE_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# 기능 플래그 (true/false)
|
||||||
|
# 백엔드 엔드포인트 준비 상태에 따라 개별 화면 제어에 활용
|
||||||
|
FEATURE_VENDORS_ENABLED=false
|
||||||
|
FEATURE_PRODUCTS_ENABLED=false
|
||||||
|
FEATURE_WAREHOUSES_ENABLED=false
|
||||||
|
FEATURE_CUSTOMERS_ENABLED=false
|
||||||
|
FEATURE_USERS_ENABLED=false
|
||||||
|
FEATURE_GROUPS_ENABLED=false
|
||||||
|
FEATURE_MENUS_ENABLED=false
|
||||||
|
FEATURE_GROUP_PERMISSIONS_ENABLED=false
|
||||||
|
FEATURE_APPROVALS_ENABLED=false
|
||||||
|
FEATURE_ZIPCODE_SEARCH_ENABLED=false
|
||||||
13
.env.production.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
API_BASE_URL=https://api.superport.example.com
|
||||||
|
|
||||||
|
# 기능 플래그 (true/false)
|
||||||
|
FEATURE_VENDORS_ENABLED=true
|
||||||
|
FEATURE_PRODUCTS_ENABLED=true
|
||||||
|
FEATURE_WAREHOUSES_ENABLED=true
|
||||||
|
FEATURE_CUSTOMERS_ENABLED=true
|
||||||
|
FEATURE_USERS_ENABLED=true
|
||||||
|
FEATURE_GROUPS_ENABLED=true
|
||||||
|
FEATURE_MENUS_ENABLED=true
|
||||||
|
FEATURE_GROUP_PERMISSIONS_ENABLED=true
|
||||||
|
FEATURE_APPROVALS_ENABLED=true
|
||||||
|
FEATURE_ZIPCODE_SEARCH_ENABLED=true
|
||||||
49
.gitignore
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
|
|
||||||
|
# Env files
|
||||||
|
.env.*
|
||||||
|
!.env.*.example
|
||||||
45
.metadata
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: android
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: ios
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: linux
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: macos
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: web
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: windows
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
99
AGENTS.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
Place all Flutter source in `lib/`, keeping cross-cutting pieces in `lib/core/` (config, constants, services) and feature screens under `lib/features/<domain>/`. Shared widgets and theming go in `lib/widgets/` and `lib/theme/`. Tests mirror this layout in `test/` and, when browser flows matter, `integration_test/`. Static specs, including the UI breakdown in `doc/입출고 대여 폼 정리.md`, stay in `doc/`. Keep assets in `assets/` and register them inside `pubspec.yaml`.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
- `flutter pub get` — install or refresh package dependencies.
|
||||||
|
- `flutter analyze` — static analysis; treat warnings as blockers before review.
|
||||||
|
- `flutter test` — run the unit/widget suite; add `--coverage` when validating overall health.
|
||||||
|
- `flutter run -d chrome --web-renderer canvaskit` — local web run matching production rendering.
|
||||||
|
- `dart run build_runner build --delete-conflicting-outputs` — regenerate freezed/json_serializable files when models change.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
Use Flutter’s two-space indentation and run `dart format .` before committing. Follow the Clean Architecture layering: DTOs/remote in `data`, domain interfaces/use cases in `domain`, controllers/widgets in presentation. File names use `snake_case.dart`; classes use `UpperCamelCase`; methods and fields use `lowerCamelCase`. Prefer `const` constructors/widgets, and use `shadcn_ui` components (especially `ShadTable`) for new screens. Register dependencies in `lib/injection_container.dart` via `get_it`.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
Each feature ships with unit tests (`*_test.dart`) living beside the source module. Widget tests should verify key flows like list rendering and form validation; leverage `WidgetTester` with fake data. Integration scenarios covering navigation across the inventory, approval, and reporting flows belong in `integration_test/`. Target meaningful coverage (≈70% per feature) and ensure CI-friendly seeds by avoiding real API calls.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
Commits follow the existing Superport convention: Korean imperative summaries with optional English technical nouns, e.g., `"대여 상세 테이블 정렬 수정"`. For PRs, include (1) a concise summary of user-visible impact, (2) screenshots or GIFs for UI changes, (3) linked issue or JIRA reference, and (4) verification notes (commands run, tests passing). Squash before merge unless release tagging requires history.
|
||||||
|
|
||||||
|
## Architecture & Environment Notes
|
||||||
|
Initialize environments via `.env.development` / `.env.production` and load them through `Environment.initialize()` before bootstrapping DI. New data sources should expose repository interfaces in `domain/` and rely on the shared `ApiClient` instance. Do not use mock data in the application; always call the real backend (staging/production as appropriate). If an endpoint is not available, mark the feature as disabled behind a feature flag rather than mocking.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Language & Communication Policy
|
||||||
|
|
||||||
|
- This document (AGENTS.md) and repository guidelines are written in English.
|
||||||
|
- Assistant responses to users (conversations, task summaries, PR descriptions) must be in Korean by default, unless the user explicitly requests English.
|
||||||
|
- Code comments must be written in Korean (see “Commenting Policy — Korean”).
|
||||||
|
- User-facing UI copy is Korean-first unless a requirement states otherwise.
|
||||||
|
- Identifiers (class/variable/function/file names) remain in English following the naming conventions above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SRP & Clean Architecture Enforcement
|
||||||
|
|
||||||
|
Apply these principles repo-wide. Use the checklist below during reviews.
|
||||||
|
|
||||||
|
- Single Responsibility Principle (SRP)
|
||||||
|
- A class/file must have exactly one reason to change.
|
||||||
|
- In widgets, separate layout/rendering from state/event handling. Move complex logic to controllers/use cases.
|
||||||
|
- Extract form validation, transformations/mapping, sorting/filtering into dedicated utilities/services.
|
||||||
|
- Avoid god pages/widgets (split when file > 400 LOC or a build method > 200 LOC).
|
||||||
|
|
||||||
|
- Clean Architecture boundaries
|
||||||
|
- presentation: views/controllers/state (view models). No business rules or direct data access.
|
||||||
|
- domain: entities, repository interfaces, use cases. No framework dependencies.
|
||||||
|
- data: DTOs and data sources (API/local) implementing domain interfaces. Never depend on presentation.
|
||||||
|
- DI: wire dependencies only in `lib/injection_container.dart`. No cross-layer back references.
|
||||||
|
|
||||||
|
- Dependency rule
|
||||||
|
- Allowed direction is presentation → domain → data; never the reverse.
|
||||||
|
- domain must not depend on `flutter/*`, `shadcn_ui`, or any UI packages.
|
||||||
|
- data must not reference presentation. Convert DTOs to domain entities before exposing.
|
||||||
|
|
||||||
|
- UI composition (shadcn_ui)
|
||||||
|
- Lists: standardize on `ShadTable`-based components; keep table/column specs in dedicated classes.
|
||||||
|
- Forms: separate field widgets, validation logic, and dialog layout; manage form state in a controller.
|
||||||
|
- Modals: use `SuperportShadDialog` consistently with header/body/footer sections.
|
||||||
|
|
||||||
|
- Testing
|
||||||
|
- presentation: widget tests for rendering, interactions, and validation messages.
|
||||||
|
- domain: unit tests for use-case scenarios.
|
||||||
|
- data: contract tests for JSON mapping; never hit real network endpoints.
|
||||||
|
|
||||||
|
## Networking Stack Guidelines
|
||||||
|
|
||||||
|
- Use a single `ApiClient` (Dio wrapper) for all HTTP calls; do not instantiate raw `Dio` per feature.
|
||||||
|
- Initialize env via `Environment.initialize()` before DI, then register `ApiClient` in `lib/injection_container.dart` and inject into repositories.
|
||||||
|
- Attach `AuthInterceptor` to handle token injection and 401 → refresh → retry. On refresh failure, clear session and navigate to login.
|
||||||
|
- Standardize query params: `page`, `page_size`, `q`, `sort`, `order`, `updated_since`, `include`.
|
||||||
|
- Keep logging in development only and redact secrets (tokens/cookies).
|
||||||
|
|
||||||
|
## Commenting Policy — Korean
|
||||||
|
|
||||||
|
- Write comments in Korean for all public APIs, core logic, and business rules.
|
||||||
|
- Public classes/methods: use `///` doc comments in Korean (purpose, parameters, returns, exceptions, examples).
|
||||||
|
- Complex branches/math/performance: use `//` line comments to explain intent and rationale.
|
||||||
|
- Prefer Korean domain terms; include the English term in parentheses on first mention (e.g., 결재(approval)).
|
||||||
|
- Don’t: add meaningless comments, let comments drift from code, or mix excessive English.
|
||||||
|
- Do: document invariants, preconditions, side effects, and failure scenarios explicitly.
|
||||||
|
|
||||||
|
## File/Function Size Guidelines
|
||||||
|
|
||||||
|
- Files over ~400 LOC: consider splitting (widgets/controllers/utils).
|
||||||
|
- Functions over ~60 LOC: refactor into private helpers.
|
||||||
|
- Widget tree nesting > 5 levels: extract sub-widgets.
|
||||||
|
|
||||||
|
## Review Checklist (SRP/Clean)
|
||||||
|
|
||||||
|
- [ ] Does this file/class have exactly one reason to change?
|
||||||
|
- [ ] Are business rules absent from presentation?
|
||||||
|
- [ ] Does the domain layer avoid dependencies on frameworks/UI packages?
|
||||||
|
- [ ] Does the data layer avoid referencing presentation?
|
||||||
|
- [ ] Is DI centralized in `lib/injection_container.dart`?
|
||||||
|
- [ ] Are validation/transforms/sort/filter extracted into utilities/services?
|
||||||
|
- [ ] Are comments written in Korean and do they clearly cover intent/constraints/failure cases?
|
||||||
42
README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Superport v2
|
||||||
|
|
||||||
|
간단 입·출고 + 결재 시스템(웹) UI.
|
||||||
|
|
||||||
|
## 환경 설정
|
||||||
|
|
||||||
|
1) 환경 파일 준비 (.env)
|
||||||
|
|
||||||
|
루트에 다음 파일을 생성하세요(예시 제공):
|
||||||
|
|
||||||
|
- `.env.development` → 개발 환경
|
||||||
|
- `.env.production` → 운영 환경
|
||||||
|
|
||||||
|
`*.example` 파일을 복사하여 값을 채우면 됩니다.
|
||||||
|
|
||||||
|
핵심 키:
|
||||||
|
|
||||||
|
- `API_BASE_URL` — 백엔드 API 베이스 URL
|
||||||
|
- `FEATURE_*` — 기능 플래그 (예: `FEATURE_VENDORS_ENABLED`)
|
||||||
|
|
||||||
|
2) 의존성 설치
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
3) 개발 실행 (웹)
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter run -d chrome --web-renderer canvaskit --dart-define=ENV=development
|
||||||
|
```
|
||||||
|
|
||||||
|
## 구조
|
||||||
|
|
||||||
|
- `lib/core/` — 공통 구성(환경, 네트워크, 라우팅)
|
||||||
|
- `lib/features/<domain>/` — 기능별 폴더 (clean architecture: domain/data/presentation)
|
||||||
|
- `lib/widgets/` — 공용 위젯
|
||||||
|
|
||||||
|
## 빌드/검증
|
||||||
|
|
||||||
|
- `flutter analyze` — 정적 분석
|
||||||
|
- `flutter test` — 단위/위젯 테스트
|
||||||
28
analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
14
android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
44
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.superport_v2"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.example.superport_v2"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
45
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="superport_v2"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.example.superport_v2
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
24
android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
3
android/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||||
26
android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.9.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
151
doc/API_CLIENT_SPEC.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# ApiClient 설계서 (Dio 기반, Superport 스타일)
|
||||||
|
|
||||||
|
본 문서는 Superport 레포 스타일과 동일한 인증/네트워킹 패턴을 본 프로젝트에 적용하기 위한 ApiClient 설계를 정의한다. 실제 구현은 이후 단계에서 진행한다(문서 선정리).
|
||||||
|
|
||||||
|
## 1) 목표
|
||||||
|
- 단일 진입점 ApiClient(Dio 래퍼)로 모든 네트워크 호출 일원화
|
||||||
|
- 환경 변수 기반 BaseURL/타임아웃/로그 레벨 설정
|
||||||
|
- 인증 토큰 주입, 401 자동 처리(토큰 갱신 → 재시도), 에러 매핑 일관화
|
||||||
|
- 목록/단건 표준 응답 구조에 맞춘 헬퍼 제공
|
||||||
|
|
||||||
|
## 2) 의존성(추가 예정)
|
||||||
|
- dio: ^5.x (HTTP 클라이언트)
|
||||||
|
- get_it: ^7.x (DI) — 이미 사용 중
|
||||||
|
- flutter_secure_storage(or web localStorage 대체): 토큰 저장(플랫폼별 분기)
|
||||||
|
- intl: ^0.20.x (기존)
|
||||||
|
- 개발 전용: pretty_dio_logger(선택)
|
||||||
|
|
||||||
|
## 3) 환경 변수
|
||||||
|
- API_BASE_URL: 예) https://api.example.com/api/v1
|
||||||
|
- API_CONNECT_TIMEOUT_MS: 예) 15000
|
||||||
|
- API_RECEIVE_TIMEOUT_MS: 예) 30000
|
||||||
|
- LOG_LEVEL: debug|info|warn|error
|
||||||
|
|
||||||
|
로드 순서: `await Environment.initialize()` → DI에서 ApiClient 생성 시 사용
|
||||||
|
|
||||||
|
## 4) 인증 방식(슈퍼포트와 동일)
|
||||||
|
- 로그인: `POST /auth/login` → `{ data: { token: string, user?: {...} } }`
|
||||||
|
- 요청 헤더: `Authorization: Bearer <token>`
|
||||||
|
- 토큰 저장: 보안 저장소(모바일)/localStorage(웹) 또는 httpOnly 쿠키(백엔드 정책에 따름)
|
||||||
|
- 토큰 갱신(선택): `POST /auth/refresh` → `{ data: { token: string } }`
|
||||||
|
- 401 처리: `AuthInterceptor`가 401 수신 시 자동 갱신 → 원요청 재시도(1회). 갱신 실패 시 로그아웃/세션 초기화 및 로그인 화면 이동
|
||||||
|
|
||||||
|
## 5) 에러 매핑 정책
|
||||||
|
- 400 BAD_REQUEST: 검증 오류 → 필드 에러로 매핑
|
||||||
|
- 404 NOT_FOUND: 리소스 없음
|
||||||
|
- 409 CONFLICT: 유니크 충돌/상태 충돌
|
||||||
|
- 422 UNPROCESSABLE_ENTITY: 비즈니스 규칙 위반(예: 출고 고객 미선택, blocking 전이)
|
||||||
|
- 500+: 서버 오류 → 공통 메시지 + 로그 수집
|
||||||
|
- 표준 포맷: `{ error: { code, message, details? } }` 수용. 비표준 응답은 DioException 메시지로 대체
|
||||||
|
|
||||||
|
## 6) 쿼리 규약/헬퍼
|
||||||
|
- 페이지네이션: `page`, `page_size`
|
||||||
|
- 정렬: `sort`, `order=asc|desc`
|
||||||
|
- 검색: `q`
|
||||||
|
- 증분: `updated_since`
|
||||||
|
- include 확장: `include=lines,customers,approval` 등
|
||||||
|
- 헬퍼: `buildQuery({page, pageSize, q, sort, order, include, filters})`
|
||||||
|
|
||||||
|
## 7) ApiClient 스켈레톤(인터페이스)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
/// 네트워크 공통 클라이언트 (Dio 래퍼)
|
||||||
|
class ApiClient {
|
||||||
|
// 내부 Dio 인스턴스(외부 사용 금지, 필요한 경우 read-only 게터 제공)
|
||||||
|
final Dio _dio;
|
||||||
|
|
||||||
|
ApiClient({required Dio dio}) : _dio = dio;
|
||||||
|
|
||||||
|
Dio get dio => _dio; // 과도한 사용은 지양하고, 가능하면 아래 헬퍼 사용
|
||||||
|
|
||||||
|
Future<Response<T>> get<T>(
|
||||||
|
String path, {
|
||||||
|
Map<String, dynamic>? query,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Response<T>> post<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? query,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Response<T>> patch<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? query,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Response<T>> delete<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? query,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
구현 시 기본 옵션
|
||||||
|
- BaseOptions: baseUrl, connectTimeout, receiveTimeout
|
||||||
|
- 공통 헤더: `Accept: application/json`, `Authorization: Bearer <token?>`
|
||||||
|
- Interceptors:
|
||||||
|
- `AuthInterceptor`(요청 전 토큰 주입, 401에서 갱신/재시도)
|
||||||
|
- `LoggingInterceptor`(개발 모드에서만)
|
||||||
|
|
||||||
|
## 8) Interceptor 설계
|
||||||
|
- AuthInterceptor
|
||||||
|
- 요청: 저장된 토큰이 있으면 `Authorization` 헤더 추가
|
||||||
|
- 응답: 401이면 1) 갱신 중 동시성 잠금 2) 갱신 성공 시 대기 중 요청 재시도 3) 실패 시 토큰 삭제/로그아웃
|
||||||
|
- Retry 정책: 재시도는 1회, idempotent GET/HEAD 위주. POST/PATCH는 401 갱신 후 재시도 1회만 허용
|
||||||
|
|
||||||
|
## 9) 표준 응답 파서
|
||||||
|
- 목록: `{ items: [...], page, page_size, total }`
|
||||||
|
- 단건: `{ data: {...} }`
|
||||||
|
- 제네릭 파서 유틸 제공: `parseList<T>(res, fromJson)`, `parseItem<T>(res, fromJson)`
|
||||||
|
|
||||||
|
## 10) 샘플 사용 (Repository)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class VendorRepositoryImpl implements VendorRepository {
|
||||||
|
final ApiClient api;
|
||||||
|
VendorRepositoryImpl(this.api);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Paged<Vendor>> list({int page = 1, int pageSize = 20, String? q}) async {
|
||||||
|
final res = await api.get('/vendors', query: { 'page': page, 'page_size': pageSize, if (q != null) 'q': q });
|
||||||
|
return parseList<Vendor>(res.data, Vendor.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Vendor> create(VendorCreate body) async {
|
||||||
|
final res = await api.post('/vendors', data: body.toJson());
|
||||||
|
return parseItem<Vendor>(res.data, Vendor.fromJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11) 보안/스토리지
|
||||||
|
- 토큰 저장: 플랫폼별로 적합한 저장소 사용(웹은 localStorage, 모바일은 secure storage)
|
||||||
|
- 민감정보 로깅 금지(토큰/쿠키 마스킹)
|
||||||
|
- CORS/쿠키 기반 인증 사용 시, Dio 요청에 `withCredentials=true` 설정 필요(백엔드 정책에 따름)
|
||||||
|
|
||||||
|
## 12) 테스트 전략
|
||||||
|
- 위젯/도메인 테스트: 네트워크 의존 제거(리포지토리를 테스트 더블로 대체)
|
||||||
|
- 통합 테스트: 실제 스테이징 API를 사용하여 로그인→호출→401→갱신→재시도 플로우 검증
|
||||||
|
|
||||||
|
## 13) 구현 순서 요약(체크)
|
||||||
|
- [ ] pubspec에 `dio`(필수), `pretty_dio_logger`(개발) 추가
|
||||||
|
- [ ] `ApiClient`/`AuthInterceptor` 스켈레톤 작성
|
||||||
|
- [ ] `Environment.initialize()` → `get_it` DI에서 ApiClient 생성/주입
|
||||||
|
- [ ] 리포지토리 구현에서 ApiClient 사용으로 통일(직접 Dio 인스턴스화 금지)
|
||||||
|
- [ ] 에러/토큰/재시도 정책 위젯 레벨 연결(토스트/로그아웃)
|
||||||
|
|
||||||
|
참고
|
||||||
|
- Superport 레포: `.env`의 `API_BASE_URL`, `test_api_integration.sh`의 `/auth/login` + Bearer 사용
|
||||||
|
- 본 프로젝트: AGENTS.md의 “Do not use mock data” 및 DI/레이어 경계 정책 준수
|
||||||
119
doc/IMPLEMENTATION_TASKS.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Superport v2 프론트엔드 구현 Task List (UI First → API Integration)
|
||||||
|
|
||||||
|
본 체크리스트는 PRD(`doc/PRD_입출고_결재_v2.md`)를 기준으로 shadcn_ui 스타일과 반응형 패턴을 준수하여 화면을 구현하기 위한 단계별 작업 목록입니다. 작업 순서는 ① 코드 시작 전 최종 확인 → ② UI 스캐폴딩/상호작용 구현 → ③ 실제 API 연동(Dio/ApiClient/DI)입니다. Mock 데이터는 사용하지 않습니다.
|
||||||
|
|
||||||
|
## 0) 코드 시작 전 최종 확인(Repository/환경)
|
||||||
|
- [ ] Flutter 버전/채널 확인, `flutter pub get`
|
||||||
|
- [ ] `pubspec.yaml` 확인: `go_router`, `shadcn_ui`, `intl`, `two_dimensional_scrollables`, `lucide_icons_flutter` 포함
|
||||||
|
- [ ] `Environment.initialize()` 호출 및 `.env.development`/`.env.production`에서 `API_BASE_URL`/`TIMEOUT_MS`/`LOG_LEVEL` 로드
|
||||||
|
- [ ] 라우팅 스켈레톤(go_router) 구성: 로그인/대시보드/입·출·대여/마스터/결재/보고서
|
||||||
|
- [ ] 글로벌 테마/ShadTheme 확인(폰트/간격/배지/버튼 일관)
|
||||||
|
|
||||||
|
## 1) 공통 컴포넌트/레이아웃(UI)
|
||||||
|
- [ ] AppLayout(좌 사이드바/상단 헤더/본문) 적용, 브레드크럼·타이틀·툴바 영역 정리
|
||||||
|
- [ ] 테이블: `ShadTable.list` 표준화(고정 헤더/가로 스크롤/소팅/페이지네이션 UI만)
|
||||||
|
- [ ] 모달: `SuperportShadDialog`(헤더/본문/푸터 분리, 모바일 풀스크린) 공통 wrapper
|
||||||
|
- [ ] 입력 위젯: `ShadInput/Select/Switch`, `SuperportShadDatePicker/RangePicker` 적용 가이드
|
||||||
|
- [ ] 필터바(검색/기간/상태/창고/Reset) 공통 위젯
|
||||||
|
- [ ] 반응형 프리셋: 데스크톱/태블릿/모바일 열 가시성 설정(섹션 12 규칙 반영)
|
||||||
|
- [ ] 토스트/스낵바/스켈레톤/Empty 상태 공통 처리
|
||||||
|
|
||||||
|
## 2) 인증/대시보드(UI)
|
||||||
|
- [ ] 로그인 화면(`/login`): 아이디/비밀번호 UI(제출/로딩/에러 표시 흐름)
|
||||||
|
- [ ] 대시보드(`/`): KPI 카드, 최근 트랜잭션, 내 결재 대기 리스트 — 스켈레톤/Empty 상태 구현
|
||||||
|
|
||||||
|
## 3) 입고(`/inbounds`) UI
|
||||||
|
- [ ] 라우트/네비게이션 연결
|
||||||
|
- [ ] 목록 테이블: 번호/처리일자/창고/트랜잭션번호/상태/작성자/품목수/총수량/비고
|
||||||
|
- [ ] 필터: 기간/창고/상태/검색, 소팅/페이지네이션
|
||||||
|
- [ ] 신규 모달: 헤더(처리일자/창고/상태/작성자/비고) + 시스템 필드(`transaction_type_id=입고` RO/숨김)
|
||||||
|
- [ ] 라인 테이블: 제품(자동완성)→제조사/단위 자동, 수량/단가/비고, (+)/(−) 행 편집
|
||||||
|
- [ ] 검증: 필수/수량>=1/단가>=0, 상단 요약 + 인라인 에러
|
||||||
|
- [ ] 수정 모달: 작성자/트랜잭션번호 RO, 종결 상태 수정 제한
|
||||||
|
|
||||||
|
## 4) 출고(`/outbounds`) UI
|
||||||
|
- [ ] 목록 테이블: 번호/처리일자/창고/트랜잭션번호/상태/작성자/고객수/품목수/총수량/비고
|
||||||
|
- [ ] 필터: 기간/창고/상태/고객/검색
|
||||||
|
- [ ] 신규/수정 모달: 헤더(…)+ 시스템 필드(`transaction_type_id=출고` RO/숨김)
|
||||||
|
- [ ] 고객 연결(멀티): 고객사 자동완성 → 토큰/칩 UI, 최소 1건 검증
|
||||||
|
- [ ] 라인 테이블: 제품/제조사RO/단위RO/수량/단가/비고
|
||||||
|
|
||||||
|
## 5) 대여(`/rentals`) UI
|
||||||
|
- [ ] 목록 테이블: 번호/처리일자/창고/대여구분/트랜잭션번호/상태/반납예정일/고객수/품목수/비고
|
||||||
|
- [ ] 필터: 기간/창고/상태/대여구분/반납예정일 범위/검색
|
||||||
|
- [ ] 신규/수정 모달: 헤더(…/대여구분/반납예정일) + 시스템 필드(`transaction_type_id` 대여/반납 자동 매핑)
|
||||||
|
- [ ] 고객 연결(멀티) + 라인 테이블(입고·출고와 동일)
|
||||||
|
- [ ] 규칙: 대여구분은 종결 후 변경 불가, 반납예정일은 진행 중 수정 가능
|
||||||
|
|
||||||
|
## 6) 마스터(UI)
|
||||||
|
- [ ] 벤더: 목록/필터(q/사용여부), 신규/수정(코드RO), 삭제/복구 UI
|
||||||
|
- [ ] 제품: 목록/필터(q/제조사/단위/사용), 신규/수정(코드RO)
|
||||||
|
- [ ] 창고: 목록/필터(q/사용), 신규/수정(우편번호 검색 모달 UI 연동)
|
||||||
|
- [ ] 고객사: 목록/필터(q/유형/사용), 신규/수정(유형→is_partner/is_general 매핑 UI)
|
||||||
|
- [ ] 사용자: 목록/필터(q/그룹/사용), 신규/수정(사번RO)
|
||||||
|
- [ ] 그룹: 목록/필터(q/기본/사용), 신규/수정(그룹명RO)
|
||||||
|
- [ ] 메뉴: 목록/필터(q/상위/사용), 신규/수정(메뉴코드RO)
|
||||||
|
- [ ] 그룹 권한: 목록/필터(그룹/메뉴/사용), 체크박스 매트릭스 편집 UI
|
||||||
|
|
||||||
|
## 7) 결재(UI)
|
||||||
|
- [ ] 결재(`/approvals`): 목록/필터, 상세(개요/단계/이력 탭)
|
||||||
|
- [ ] 템플릿 불러오기: 단계 탭에서 템플릿 선택 UI(단계 리스트 반영)
|
||||||
|
- [ ] 단계 행위: 승인/반려/코멘트 버튼(가능 여부 상태에 따라 비활성/툴팁)
|
||||||
|
- [ ] 단계 관리(`/approval-steps`): 목록/편집(신규/수정)
|
||||||
|
- [ ] 이력(`/approval-histories`): 조회 전용 테이블
|
||||||
|
- [ ] 템플릿(`/approval-templates`): 목록/헤더+단계 반복 폼
|
||||||
|
|
||||||
|
## 8) 우편번호 검색 모달(UI)
|
||||||
|
- [ ] 입력: 검색어 텍스트
|
||||||
|
- [ ] 결과 테이블: 우편번호/시도/시군구/도로명/건물번호
|
||||||
|
- [ ] 선택 시: 부모 폼 `zipcode`/주소 구성요소 채움
|
||||||
|
|
||||||
|
## 9) 보고서(`/reports`) UI
|
||||||
|
- [ ] 조건 폼: 기간/유형/창고/상태
|
||||||
|
- [ ] 액션: XLSX/PDF 버튼 — 미제공 시 버튼 비활성 UI(연동은 API 단계에서)
|
||||||
|
|
||||||
|
## 10) 데이터 계층/상태 관리
|
||||||
|
- [ ] 리포지토리 인터페이스(domain) 정의 및 구현(data): 트랜잭션/결재/마스터/룩업/우편번호
|
||||||
|
- [ ] DTO ↔ 도메인 엔티티 매핑(응답 `{ items }`/`{ data }` 구조 표준화)
|
||||||
|
- [ ] 페이지네이션 상태(현재 페이지/사이즈/전체) 및 필터 상태 싱크(go_router querystring 연동)
|
||||||
|
- [ ] 정렬/검색/Include 옵션 직렬화 및 유지
|
||||||
|
|
||||||
|
## 11) API 연동 단계(Dio/ApiClient/DI)
|
||||||
|
- [ ] 네트워킹 패키지 추가: `dio:^5.x`, `pretty_dio_logger`(dev 선택), 토큰 저장용 `flutter_secure_storage`(모바일)/웹 스토리지
|
||||||
|
- [ ] `ApiClient`/`AuthInterceptor` 스켈레톤 작성(설계: `doc/API_CLIENT_SPEC.md`)
|
||||||
|
- [ ] `Environment.initialize()` → `get_it` DI에서 ApiClient 생성/주입
|
||||||
|
- [ ] 공통 에러 매핑(400/404/409/422) 및 토스트/필드 바인딩 연결
|
||||||
|
- [ ] 메뉴/권한 로딩 → 버튼/액션 노출 제어
|
||||||
|
- [ ] 각 화면 API 연결:
|
||||||
|
- 입고/출고/대여: 목록/상세/생성/수정/삭제/복구 + include/필터/정렬/페이지네이션
|
||||||
|
- 마스터: vendors/products/warehouses/customers/employees/menus/groups/group-permissions(+ 일괄 저장)
|
||||||
|
- 결재: approvals(+steps/histories), actions, can-proceed, 템플릿 CRUD/단계 배치
|
||||||
|
- 우편번호: `GET /zipcodes?...`
|
||||||
|
- 보고서: 다운로드 엔드포인트 연동(제공 시)
|
||||||
|
|
||||||
|
## 12) 검증/접근성/상호작용
|
||||||
|
- [ ] 필수/형식/업무 규칙 검증(출고/대여 고객 최소 1건 등)
|
||||||
|
- [ ] 키보드: Esc 닫기, Enter 제출/셀 이동, Tab 포커스 이동, 포커스 트랩
|
||||||
|
- [ ] 합계/요약 배지 실시간 반영(수량/단가 변경 시)
|
||||||
|
|
||||||
|
## 13) 반응형/열 가시성
|
||||||
|
- [ ] 데스크톱/태블릿/모바일 프리셋 구현(PRD 섹션 12 규칙 적용)
|
||||||
|
- [ ] 모바일 카드형 요약(핵심 3~4필드) 구성
|
||||||
|
|
||||||
|
## 14) 테스트/품질
|
||||||
|
- [ ] `flutter analyze` 경고 0
|
||||||
|
- [ ] 위젯 테스트: 테이블 렌더/필터/페이지네이션/모달 열기/검증 메시지
|
||||||
|
- [ ] 내비 통합 테스트(선택): 로그인 → 대시보드 → 입/출/대여 → 결재 → 마스터
|
||||||
|
- [ ] `dart format .` 적용
|
||||||
|
|
||||||
|
## 15) Definition of Done(DoD)
|
||||||
|
- [ ] 모든 목록/폼/모달/필터/페이지네이션 동작
|
||||||
|
- [ ] 모바일/태블릿/데스크톱 레이아웃 검증(핵심 열 가시성)
|
||||||
|
- [ ] 실제 API로 주요 플로우(신규/수정/삭제/복구) 검증 완료
|
||||||
|
- [ ] 문서 최신화(PRD/체크리스트)
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
- PRD: `doc/PRD_입출고_결재_v2.md`
|
||||||
|
- 사양: `doc/stock_approval_system_spec_v4.md`, `doc/stock_approval_system_api_v4.md`
|
||||||
|
- 네트워킹 설계: `doc/API_CLIENT_SPEC.md`
|
||||||
|
- 스타일 참조: `/Users/maximilian.j.sul/Documents/flutter/superport`
|
||||||
538
doc/PRD_입출고_결재_v2.md
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
# Superport v2 입·출고 + 결재 시스템 PRD (Draft v1)
|
||||||
|
|
||||||
|
본 문서는 `doc/입출고 대여 폼 정리.md`, `doc/stock_approval_system_spec_v4.md`, `doc/stock_approval_system_api_v4.md`를 근거로 Flutter 웹 프론트엔드 구현을 위한 상세 요구사항을 정리한다. 스타일은 `/Users/maximilian.j.sul/Documents/flutter/superport`의 UI/UX 패턴을 준수한다.
|
||||||
|
|
||||||
|
## 1. 범위 및 목표
|
||||||
|
- 범위: 로그인/대시보드/입고/출고/대여/마스터(벤더, 제품, 창고, 고객사, 사용자, 그룹, 메뉴, 권한)/결재(결재, 단계, 이력, 템플릿)/보고서.
|
||||||
|
- 목표: 백엔드 API와 연동하여 화면/네비게이션/상호작용(폼 검증, 필터, 페이징, 모달)을 완성. DTO/리포지토리 인터페이스를 분리해 유지보수를 용이하게 함.
|
||||||
|
- 기술: Flutter + shadcn_ui + go_router + two_dimensional_scrollables. 반응형 웹 최적화.
|
||||||
|
- 제약: 입력·수정 폼은 팝업 모달(ShadDialog 기반)로 노출. 테이블은 ShadTable 계열 사용.
|
||||||
|
|
||||||
|
## 2. 전역 UX/스타일 가이드
|
||||||
|
- 레이아웃(AppLayout): 좌측 사이드바 내비게이션 + 상단 헤더(타이틀, 브레드크럼, 액션) + 콘텐츠.
|
||||||
|
- 컴포넌트(슈퍼포트 준수):
|
||||||
|
- 테이블: `ShadTable.list`(고정 헤더, 필요 시 첫 컬럼 고정, 가로 스크롤 허용).
|
||||||
|
- 버튼: `ShadButton.(primary|outline|ghost)`, 행 액션은 `ghost` 아이콘 버튼.
|
||||||
|
- 입력: `ShadInput`, `ShadSelect`, `SuperportShadDatePicker/DateRangePicker`, `ShadSwitch`, `ShadBadge`, `ShadTooltip`.
|
||||||
|
- 모달: `SuperportShadDialog`(헤더/본문/푸터 구분, ESC/배경 클릭 닫기 옵션).
|
||||||
|
- 페이징: 테이블 하단 우측 `page / perPage / total` 표시 + 페이지네이션.
|
||||||
|
- 필터/검색 바: 테이블 상단. 기본 `검색(q)` + 주요 조건(기간/상태/창고 등). `Reset` 버튼 제공.
|
||||||
|
- 정렬: 테이블 헤더 클릭 1열 정렬(추후 멀티 정렬 확장 가능).
|
||||||
|
- 반응형:
|
||||||
|
- Breakpoints: `>=1280` 데스크톱 3열, `>=1024` 2열, `>=768` 2열(compact), `<768` 1열 카드형 목록 대체 및 모달 전면(full-screen) 전환.
|
||||||
|
- 테이블은 작은 화면에서 핵심 컬럼만 우선 노출(열 가시성 프리셋), 나머지는 가로 스크롤.
|
||||||
|
- 접근성: 포커스 트랩, 키보드 내비게이션(Tab/Enter/Esc), 명확한 에러 메시지.
|
||||||
|
- 로딩/빈 상태: Skeleton과 Empty 상태 메시지(“조건을 변경해 보세요”). 오류는 Toaster로 노출.
|
||||||
|
|
||||||
|
## 3. 권한/메뉴 정책(뷰 관점)
|
||||||
|
- 그룹 기반 메뉴 권한(`group_menu_permissions`)은 서버 응답을 기반으로 적용: `can_create/read/update/delete`에 따라 버튼/액션 노출 제어.
|
||||||
|
- 메뉴 트리: 대시보드, 입고, 출고, 대여, 마스터(벤더, 제품, 창고, 고객사, 사용자, 그룹, 메뉴, 권한), 결재(결재, 단계, 이력, 템플릿), 보고서.
|
||||||
|
|
||||||
|
## 4. 공통 데이터 소스 매핑(드롭다운/자동입력)
|
||||||
|
- 공통 룩업:
|
||||||
|
- 단위: `/uoms` → `uom_name` 표시
|
||||||
|
- 트랜잭션 유형: `/transaction-types` (입고/출고/대여/반납) — 화면 진입 시 자동 지정 또는 대여구분에 따라 자동 매핑
|
||||||
|
- 트랜잭션 상태: `/transaction-statuses`(대기/진행/보류/승인/반려 등)
|
||||||
|
- 결재 상태: `/approval-statuses`(blocking/terminal 속성 포함)
|
||||||
|
- 결재 행위: `/approval-actions`(approve/reject/comment)
|
||||||
|
- 마스터:
|
||||||
|
- 창고: `/warehouses`(우편번호/주소 포함)
|
||||||
|
- 벤더: `/vendors`
|
||||||
|
- 제품: `/products?include=vendor,uom`(제품 선택 시 제조사/단위 자동 세팅, RO)
|
||||||
|
- 고객사: `/customers`(유형 플래그: 파트너/일반)
|
||||||
|
- 사원: `/employees`
|
||||||
|
- 공통 자동입력/읽기전용 규칙:
|
||||||
|
- `작성자`: 로그인 사용자로 자동 세팅(RO).
|
||||||
|
- `트랜잭션번호/결재번호`: 생성 시 자동 발번(RO)·수정 불가.
|
||||||
|
- `생성일시/변경일시`: RO.
|
||||||
|
- 제품 선택 시 `제조사/단위` 자동 세팅(RO). 수량/단가 변경 시 `합계` 자동 계산(표시용).
|
||||||
|
- 고객사 유형(파트너/일반) 선택은 내부적으로 `is_partner`/`is_general` 불린 필드로 매핑하여 저장.
|
||||||
|
|
||||||
|
## 5. 화면별 상세 사양
|
||||||
|
|
||||||
|
### 5.0 로그인/로그아웃
|
||||||
|
- 라우트: `/login`
|
||||||
|
- 목적: 세션 시작/종료(로컬 상태). 성공 시 대시보드로 이동.
|
||||||
|
- 입력(모달 아님): 아이디[TXT], 비밀번호[PWD].
|
||||||
|
- 검증: 아이디/비밀번호 필수. 실제 인증 성공 후 사용자/그룹/권한/메뉴를 서버에서 로딩.
|
||||||
|
|
||||||
|
### 5.1 대시보드
|
||||||
|
- 라우트: `/`
|
||||||
|
- 목적: 당일 입고/출고/대기 결재 위젯, 최근 트랜잭션, 내 결재 대기 요약.
|
||||||
|
- 위젯:
|
||||||
|
- KPI 배지: 오늘 입고/출고 건수, 대기 결재 수.
|
||||||
|
- 최근 트랜잭션: 번호, 일자, 유형, 상태, 작성자.
|
||||||
|
- 내 결재 대기: 결재번호, 현재 단계, 요청일시.
|
||||||
|
|
||||||
|
### 5.2 입고(Stock Inbound)
|
||||||
|
- 라우트: `/inbounds`
|
||||||
|
- 역할: 구매/반입 처리. 라인에 제품/수량/단가 입력.
|
||||||
|
- 테이블 컬럼: 번호(id), 처리일자(processed_at), 창고(warehouse_name), 트랜잭션번호(transaction_no), 상태(status_name), 작성자(employee_name), 품목수(line_count), 총수량(total_qty), 비고(note).
|
||||||
|
- 기본 정렬: `processed_at desc`.
|
||||||
|
- 필터: 기간, 창고, 상태, 검색(q: 번호/비고 등).
|
||||||
|
- 행 액션: 상세(모달), 수정(모달), 삭제(소프트), 복구.
|
||||||
|
- 신규(모달: “입고 등록”):
|
||||||
|
- 헤더 섹션: 처리일자[DT, 필수], 창고[DD, 필수], 상태[DD, 기본=대기], 작성자[자동, RO], 비고[TXT].
|
||||||
|
- 시스템 필드: 입출고유형[RO/숨김, `transaction_type_id`=입고] — 라우트 진입 시 고정 설정.
|
||||||
|
- 라인 섹션(ShadTable):
|
||||||
|
- 제품[DD/자동완성, 필수] → 선택 시 제조사[RO], 단위[RO] 자동 표시
|
||||||
|
- 수량[NUM, >=1, 필수], 단가[NUM, >=0], 비고[TXT]
|
||||||
|
- (+)행 추가, (−)행 삭제, 최소 1행 필수
|
||||||
|
- 검증: 전역 에러 바인딩(상단), 행 단위 인라인 에러.
|
||||||
|
- 수정(모달: “입고 수정”):
|
||||||
|
- 읽기전용: 작성자, 트랜잭션번호.
|
||||||
|
- 상태는 일부 제한(승인/반려 등 종결 상태는 수정 불가).
|
||||||
|
|
||||||
|
### 5.3 출고(Stock Outbound)
|
||||||
|
- 라우트: `/outbounds`
|
||||||
|
- 역할: 출하/반출 처리. 고객사 연결 필수.
|
||||||
|
- 테이블 컬럼: 번호, 처리일자, 창고, 트랜잭션번호, 상태, 작성자, 고객수, 품목수, 총수량, 비고.
|
||||||
|
- 필터: 기간, 창고, 상태, 고객사, 검색.
|
||||||
|
- 신규/수정 모달:
|
||||||
|
- 헤더: 처리일자[DT], 창고[DD], 상태[DD], 작성자[RO], 비고[TXT].
|
||||||
|
- 시스템 필드: 입출고유형[RO/숨김, `transaction_type_id`=출고] — 라우트 진입 시 고정 설정.
|
||||||
|
- 고객 연결: 고객사[DD-멀티|자동완성, 최소 1건], 비고[TXT].
|
||||||
|
- 라인: 제품[DD/자동완성], 제조사[RO], 단위[RO], 수량[NUM], 단가[NUM], 비고.
|
||||||
|
- 규칙: 고객 최소 1건 없으면 저장 불가.
|
||||||
|
|
||||||
|
### 5.4 대여(Rental)
|
||||||
|
- 라우트: `/rentals`
|
||||||
|
- 역할: 대여/반납 트랜잭션 관리.
|
||||||
|
- 테이블 컬럼: 번호, 처리일자, 창고, 대여/반납(유형), 트랜잭션번호, 상태, 반납예정일, 고객수, 품목수, 비고.
|
||||||
|
- 필터: 기간, 창고, 상태, 대여구분, 반납예정일 범위, 검색.
|
||||||
|
- 신규/수정 모달:
|
||||||
|
- 헤더: 처리일자[DT], 창고[DD], 상태[DD], 작성자[RO], 대여구분[DD: 대여/반납], 반납예정일[DT], 비고[TXT].
|
||||||
|
- 시스템 필드: 입출고유형[RO/숨김, `transaction_type_id`=대여/반납(대여구분에 따라 자동 매핑)].
|
||||||
|
- 고객 연결: 고객사[DD-멀티], 비고.
|
||||||
|
- 라인: 제품[DD/자동완성], 제조사[RO], 단위[RO], 수량[NUM], 단가[NUM], 비고.
|
||||||
|
- 규칙: 대여구분은 진행 중 제약(종결 후 변경 불가), 반납예정일은 수정 가능.
|
||||||
|
|
||||||
|
### 5.5 제조사 관리(벤더)
|
||||||
|
- 라우트: `/masters/vendors`
|
||||||
|
- 테이블: 번호, 벤더코드, 벤더명, 사용여부, 비고, 변경일시.
|
||||||
|
- 필터: 검색(q), 사용여부, 삭제 포함(고급) 토글.
|
||||||
|
- 신규(모달): 벤더코드[TXT, 고유], 벤더명[TXT], 사용여부[SW], 비고[TXT].
|
||||||
|
- 수정(모달): 벤더코드[RO], 생성/변경일시[RO].
|
||||||
|
|
||||||
|
### 5.6 장비 모델 관리(제품)
|
||||||
|
- 라우트: `/masters/products`
|
||||||
|
- 테이블: 번호, 제품코드, 제품명, 제조사, 단위, 사용여부, 비고, 변경일시.
|
||||||
|
- 필터: 검색(q), 제조사, 단위, 사용여부, 삭제 포함(고급) 토글.
|
||||||
|
- 신규: 제품코드[TXT], 제품명[TXT], 제조사[DD], 단위[DD], 사용여부[SW], 비고.
|
||||||
|
- 수정: 제품코드[RO], 생성일시[RO].
|
||||||
|
|
||||||
|
### 5.7 입고지 관리(창고)
|
||||||
|
- 라우트: `/masters/warehouses`
|
||||||
|
- 테이블: 번호, 창고코드, 창고명, 우편번호, 상세주소, 사용여부, 비고, 변경일시.
|
||||||
|
- 필터: 검색(q), 사용여부, 삭제 포함(고급) 토글.
|
||||||
|
- 신규/수정: 창고코드[TXT], 창고명[TXT], 우편번호[검색 모달], 상세주소[TXT], 사용여부[SW], 비고. 코드/일시는 RO.
|
||||||
|
- 우편번호 검색: 전용 모달(입력: 검색어 → 결과 리스트에서 선택 시 필드 채움).
|
||||||
|
|
||||||
|
### 5.8 회사 관리(고객사)
|
||||||
|
- 라우트: `/masters/customers`
|
||||||
|
- 테이블: 번호, 고객사코드, 고객사명, 유형, 이메일, 연락처, 우편번호, 상세주소, 사용여부, 비고.
|
||||||
|
- 필터: 검색(q), 유형, 사용여부, 삭제 포함(고급) 토글.
|
||||||
|
- 신규: 고객사코드[TXT], 고객사명[TXT], 유형(파트너/일반)[DD(복수 선택 가능)], 이메일[TXT], 연락처[TXT], 우편번호[검색], 상세주소[TXT], 사용여부[SW], 비고.
|
||||||
|
- 수정: 고객사코드/생성일시 RO.
|
||||||
|
|
||||||
|
### 5.9 사용자 관리(사원)
|
||||||
|
- 라우트: `/masters/employees`
|
||||||
|
- 테이블: 번호, 사번, 성명, 이메일, 연락처, 그룹, 사용여부, 비고, 변경일시.
|
||||||
|
- 필터: 검색(q), 그룹, 사용여부, 삭제 포함(고급) 토글.
|
||||||
|
- 신규: 사번[TXT], 성명[TXT], 이메일[TXT], 연락처[TXT], 그룹[DD], 사용여부[SW], 비고.
|
||||||
|
- 수정: 사번/생성일시 RO.
|
||||||
|
|
||||||
|
### 5.10 그룹 관리
|
||||||
|
- 라우트: `/masters/groups`
|
||||||
|
- 테이블: 번호, 그룹명, 설명, 기본여부, 사용여부, 비고, 변경일시.
|
||||||
|
- 필터: 검색(q), 기본여부, 사용여부, 삭제 포함(고급) 토글.
|
||||||
|
- 신규: 그룹명[TXT], 설명[TXT], 기본여부[SW], 사용여부[SW], 비고.
|
||||||
|
- 수정: 그룹명/생성일시 RO.
|
||||||
|
|
||||||
|
### 5.11 메뉴 관리
|
||||||
|
- 라우트: `/masters/menus`
|
||||||
|
- 테이블: 번호, 메뉴코드, 메뉴명, 상위메뉴, 경로, 사용여부, 비고, 변경일시.
|
||||||
|
- 필터: 검색(q), 상위메뉴, 사용여부, 삭제 포함(고급) 토글.
|
||||||
|
- 신규: 메뉴코드[TXT], 메뉴명[TXT], 상위메뉴[DD], 경로[TXT], 표시순서[NUM], 사용여부[SW], 비고.
|
||||||
|
- 수정: 메뉴코드/생성일시 RO.
|
||||||
|
|
||||||
|
### 5.12 그룹 메뉴 권한 관리
|
||||||
|
- 라우트: `/masters/group-permissions`
|
||||||
|
- 테이블: 번호, 그룹명, 메뉴명, 생성, 조회, 수정, 삭제, 사용여부, 변경일시.
|
||||||
|
- 필터: 그룹, 메뉴, 사용여부.
|
||||||
|
- 신규: 그룹[DD], 메뉴[DD], 생성/조회/수정/삭제[CHK], 사용여부[SW].
|
||||||
|
- 수정: 그룹/메뉴 RO.
|
||||||
|
|
||||||
|
### 5.13 결재 관리(Approvals)
|
||||||
|
- 라우트: `/approvals`
|
||||||
|
- 테이블: 번호, 결재번호, 트랜잭션번호, 상태, 상신자, 요청일시, 최종결정일시, 비고.
|
||||||
|
- 신규: 트랜잭션번호[DD], 결재번호[자동], 결재상태[DD(기본=대기)], 상신자[자동], 비고.
|
||||||
|
- 상세(우측 패널 또는 모달 탭):
|
||||||
|
- 개요: 현재 상태/현재 단계/상신자/요청일시/결정일시
|
||||||
|
- 단계 탭: 단계 리스트(step_order, 승인자, 상태, 배정/결정 일시) + [템플릿 불러오기] 버튼 → 템플릿 선택 후 단계 일괄 생성(`POST /approvals/{id}/steps`)
|
||||||
|
- 이력 탭: 행위/변경 전/후 상태/일시/비고
|
||||||
|
- 단계 행위(행 액션): 승인/반려/코멘트(결재 상태 규칙에 따라 전이 가능 여부 표시).
|
||||||
|
|
||||||
|
### 5.14 결재 단계 관리(Approval Steps)
|
||||||
|
- 라우트: `/approval-steps`
|
||||||
|
- 테이블: 번호, 결재ID, 단계순서, 승인자, 상태, 배정일시, 결정일시, 비고.
|
||||||
|
- 신규/수정: 결재ID[DD], 단계순서[NUM], 승인자[DD], 단계상태[DD], 비고. (결재ID/단계순서 RO 규칙 준수)
|
||||||
|
|
||||||
|
### 5.15 결재 이력 조회(Logs)
|
||||||
|
- 라우트: `/approval-histories`
|
||||||
|
- 테이블 전용: 번호, 결재ID, 단계ID, 승인자, 행위, 변경전상태, 변경후상태, 작업일시, 비고.
|
||||||
|
|
||||||
|
### 5.16 결재 템플릿 관리
|
||||||
|
- 라우트: `/approval-templates`
|
||||||
|
- 테이블: 번호, 템플릿코드, 템플릿명, 설명, 작성자, 사용여부, 변경일시.
|
||||||
|
- 신규/수정:
|
||||||
|
- 헤더: 템플릿코드[TXT], 템플릿명[TXT], 설명[TXT], 작성자[RO], 사용여부[SW], 비고[TXT].
|
||||||
|
- 단계 섹션(반복): (+추가) 순서[NUM], 승인자[DD].
|
||||||
|
|
||||||
|
### 5.17 우편번호 검색(모달)
|
||||||
|
- 트리거: 창고/고객사 폼의 우편번호 필드의 “검색”.
|
||||||
|
- 입력: 검색어[TXT]. 결과 테이블: 우편번호 | 시도 | 시군구 | 도로명 | 건물번호. 선택 시 부모 폼에 채움.
|
||||||
|
|
||||||
|
### 5.18 보고서
|
||||||
|
- 라우트: `/reports`
|
||||||
|
- 조건: 기간[DT-기간], 유형[DD], 창고[DD], 상태[DD].
|
||||||
|
- 액션: XLSX/PDF 다운로드(백엔드 제공 엔드포인트 연동, 미제공 시 버튼 비활성 처리).
|
||||||
|
|
||||||
|
## 6. 입력/수정 폼 규칙(공통)
|
||||||
|
- 모달 구조: 헤더(타이틀) / 본문(스크롤) / 푸터(취소, 저장). 모바일에서 풀스크린 모달.
|
||||||
|
- 검증: 필수값 표기(*), 저장 시 필수/형식/업무 규칙 검사. 에러는 필드 하단 및 상단 요약으로 노출.
|
||||||
|
- 자동입력: 작성자/번호/일시 RO. 제품 선택 시 제조사/단위 자동 표시. 합계/라인수는 표시만.
|
||||||
|
- 멀티 선택: 고객사 다건은 토큰/칩 형태로 표시 및 제거 지원.
|
||||||
|
- 라인 편집: 테이블 내 인라인 편집, 행 추가/삭제. 최소 1행.
|
||||||
|
|
||||||
|
## 7. API 연동 청사진(후속 단계)
|
||||||
|
- 목록: `{ items, page, page_size, total }` 포맷 사용. 쿼리: `page, page_size, q, sort, order, updated_since, include, active, deleted`.
|
||||||
|
- 단건: `{ data: { ... } }` 포맷. 생성 시 PK 미포함, 응답에 PK 포함. 수정 시 `id` 포함.
|
||||||
|
- 소프트 삭제/복구: `DELETE /{res}/{id}`, `POST /{res}/{id}/restore`.
|
||||||
|
- 엔드포인트 매핑 예:
|
||||||
|
- 입고/출고/대여: `GET /stock-transactions?include=lines,customers,approval` | `POST /stock-transactions`(헤더+라인+고객 일괄) | `PATCH /stock-transactions/{id}` | `DELETE /stock-transactions/{id}` | `POST /stock-transactions/{id}/restore`
|
||||||
|
- 결재: `GET /approvals?include=steps,histories` | `GET /approvals/{id}?include=steps,histories` | `POST /approvals/{id}/steps` | `PATCH /approvals/{id}/steps` | `POST /approval-steps/{id}/actions` | `GET /approvals/{id}/can-proceed`
|
||||||
|
- 마스터: `/vendors`, `/products`, `/warehouses`, `/customers`, `/employees`, `/menus`, `/groups`, `/group-permissions`
|
||||||
|
- 권한 일괄 갱신: `POST /groups/{id}/permissions` (체크박스 매트릭스 일괄 저장)
|
||||||
|
- 룩업: `/uoms`, `/transaction-types`, `/transaction-statuses`, `/approval-statuses`, `/approval-actions`
|
||||||
|
- 우편번호: `GET /zipcodes?zipcode=06000&road_name=세종대로` (검색 모달에서 복합 쿼리 지원)
|
||||||
|
|
||||||
|
## 8. 와이어프레임(텍스트)
|
||||||
|
- 공통 리스트 스크린
|
||||||
|
- 헤더: [타이틀] [우측: +신규, 기타액션]
|
||||||
|
- 필터바: [검색] [기간] [상태] [창고/유형 등] [Reset]
|
||||||
|
- 본문: ShadTable.list(컬럼들…) [좌측 체크박스(옵션)] [우측 행 액션]
|
||||||
|
- 하단: 페이지네이션(좌: 건수, 우: 페이지 컨트롤)
|
||||||
|
- 공통 폼 모달
|
||||||
|
- 헤더: [타이틀]
|
||||||
|
- 본문: [필드 그리드(2~3열)] [라인 테이블(필요 시)] [고객/단계 섹션]
|
||||||
|
- 푸터: [취소] [저장(primary)]
|
||||||
|
- 예시) 출고 리스트
|
||||||
|
- 필터바: 검색 | 기간 | 창고 | 상태 | 고객사 | Reset
|
||||||
|
- 테이블: 번호 | 처리일자 | 창고 | 트랜잭션번호 | 상태 | 작성자 | 고객수 | 품목수 | 총수량 | 비고 | (행액션)
|
||||||
|
- 예시) 출고 등록 모달
|
||||||
|
- 헤더 필드: 처리일자 | 창고 | 상태 | 작성자(RO) | 비고
|
||||||
|
- 고객사(멀티) 섹션: 고객사(+추가/자동완성) | 비고
|
||||||
|
- 라인 섹션: 제품(자동완성) | 제조사(RO) | 단위(RO) | 수량 | 단가 | 비고 | (+행)
|
||||||
|
|
||||||
|
## 9. 비기능 요구사항
|
||||||
|
- 성능: 가상 스크롤 또는 배치 렌더링 고려, 페이지 당 기본 20~50행.
|
||||||
|
- 국제화: 한국어 우선. 날짜/숫자 포맷 `intl` 사용.
|
||||||
|
- 코드 구조: `lib/features/<domain>/presentation` 중심, 위젯 재사용은 `lib/widgets/`에 공통화. DI는 `lib/injection_container.dart`.
|
||||||
|
- 정적 분석: `flutter analyze` 경고 0 유지. 포맷팅 `dart format .`.
|
||||||
|
|
||||||
|
## 10. 테스트(프론트)
|
||||||
|
- 위젯 테스트:
|
||||||
|
- 테이블 렌더링(컬럼/행 수), 필터 적용, 페이징 이동, 모달 열기/검증 에러 표시.
|
||||||
|
- 위젯 테스트는 네트워크 의존 없는 UI 로직 검증에 한정(실제 API는 통합 테스트에서 검증).
|
||||||
|
- 통합 테스트(선택): 주요 내비게이션(대시보드 → 입/출/대여 → 결재 → 마스터) 흐름 확인.
|
||||||
|
|
||||||
|
---
|
||||||
|
참고 문서: `doc/입출고 대여 폼 정리.md`, `doc/stock_approval_system_spec_v4.md`, `doc/stock_approval_system_api_v4.md`
|
||||||
|
|
||||||
|
## 11. 화면별 필드 매트릭스(유형/검증/자동/소스)
|
||||||
|
|
||||||
|
표기 규칙: 유형(TXT/NUM/DT/CHK/SW/RO/DD/AA=자동완성), 소스(API/룩업/없음), 기본값, 검증(필수/형식/업무), 비고.
|
||||||
|
|
||||||
|
### 11.1 입고(신규/수정 모달)
|
||||||
|
헤더 필드
|
||||||
|
| key | 라벨 | 유형 | 소스 | 기본값 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| processed_at | 처리일자 | DT | 없음 | 오늘 | 필수 | SuperportShadDatePicker |
|
||||||
|
| warehouse_id | 창고 | DD | `/warehouses` | 없음 | 필수 | 라벨=창고명 |
|
||||||
|
| status_id | 상태 | DD | `/transaction-statuses` | 대기 | 필수 | 진행/승인/반려 등 |
|
||||||
|
| created_by_id | 작성자 | RO | 로그인 | 로그인 사용자 | - | 자동 세팅 |
|
||||||
|
| note | 비고 | TXT | 없음 | - | - | 다중라인 허용 |
|
||||||
|
| transaction_type_id | 입출고유형 | RO/숨김 | `/transaction-types` | 입고 | - | 라우트로 자동 설정 |
|
||||||
|
|
||||||
|
라인 필드(반복)
|
||||||
|
| key | 라벨 | 유형 | 소스 | 기본값 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| product_id | 제품 | AA | `/products?q=` | - | 필수 | 선택 시 제조사/단위 자동 |
|
||||||
|
| vendor_name | 제조사 | RO | products.vendor | - | - | 제품 선택 시 자동 |
|
||||||
|
| uom_name | 단위 | RO | products.uom | - | - | 제품 선택 시 자동 |
|
||||||
|
| quantity | 수량 | NUM | 없음 | 1 | 필수, >=1 | 우측 정렬 |
|
||||||
|
| unit_price | 단가 | NUM | 없음 | 0 | >=0 | 우측 정렬 |
|
||||||
|
| line_note | 비고 | TXT | 없음 | - | - | |
|
||||||
|
|
||||||
|
자동 표시(요약): 품목수(line_count), 총수량(total_qty), 총금액(total_amount) — 읽기전용.
|
||||||
|
|
||||||
|
### 11.2 출고(신규/수정 모달)
|
||||||
|
헤더 필드: 입고와 동일 + 고객 연결 섹션
|
||||||
|
| key | 라벨 | 유형 | 소스 | 기본값 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| processed_at | 처리일자 | DT | - | 오늘 | 필수 | |
|
||||||
|
| warehouse_id | 창고 | DD | `/warehouses` | - | 필수 | |
|
||||||
|
| status_id | 상태 | DD | `/transaction-statuses` | 대기 | 필수 | |
|
||||||
|
| created_by_id | 작성자 | RO | 로그인 | 로그인 사용자 | - | |
|
||||||
|
| note | 비고 | TXT | - | - | - | |
|
||||||
|
| transaction_type_id | 입출고유형 | RO/숨김 | `/transaction-types` | 출고 | - | 라우트로 자동 설정 |
|
||||||
|
|
||||||
|
고객 연결(반복 가능)
|
||||||
|
| key | 라벨 | 유형 | 소스 | 기본값 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| customer_id | 고객사 | AA | `/customers?q=` | - | 최소 1건 | 토큰/칩 UI |
|
||||||
|
| customer_note | 비고 | TXT | - | - | - | |
|
||||||
|
|
||||||
|
라인 필드: 입고와 동일(제품/제조사/단위/수량/단가/비고).
|
||||||
|
|
||||||
|
업무 규칙: 고객사 최소 1건 필수, 없으면 저장 불가(422 메시지 노출).
|
||||||
|
|
||||||
|
### 11.3 대여(신규/수정 모달)
|
||||||
|
헤더 필드
|
||||||
|
| key | 라벨 | 유형 | 소스 | 기본값 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| processed_at | 처리일자 | DT | - | 오늘 | 필수 | |
|
||||||
|
| warehouse_id | 창고 | DD | `/warehouses` | - | 필수 | |
|
||||||
|
| status_id | 상태 | DD | `/transaction-statuses` | 대기 | 필수 | |
|
||||||
|
| rental_type | 대여구분 | DD | 로컬(대여/반납) | 대여 | 필수 | 종결 후 변경 불가 |
|
||||||
|
| due_date | 반납예정일 | DT | - | +7일 | 선택 | 진행 중 수정 가능 |
|
||||||
|
| created_by_id | 작성자 | RO | 로그인 | 로그인 사용자 | - | |
|
||||||
|
| note | 비고 | TXT | - | - | - | |
|
||||||
|
| transaction_type_id | 입출고유형 | RO/숨김 | `/transaction-types` | 대여/반납 | - | 대여구분에 따른 자동 매핑 |
|
||||||
|
|
||||||
|
고객 연결/라인 필드: 출고와 동일.
|
||||||
|
|
||||||
|
### 11.4 마스터: 벤더/제품/창고/고객/사용자/그룹/메뉴/권한
|
||||||
|
벤더
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| vendor_code | 벤더코드 | TXT | 필수, 고유 | 수정 RO |
|
||||||
|
| vendor_name | 벤더명 | TXT | 필수 | |
|
||||||
|
| is_active | 사용여부 | SW | - | |
|
||||||
|
| note | 비고 | TXT | - | |
|
||||||
|
|
||||||
|
제품
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| product_code | 제품코드 | TXT | 필수, 고유 | 수정 RO |
|
||||||
|
| product_name | 제품명 | TXT | 필수 | |
|
||||||
|
| vendor_id | 제조사 | DD | 필수 | `/vendors` |
|
||||||
|
| uom_id | 단위 | DD | 필수 | `/uoms` |
|
||||||
|
| is_active | 사용여부 | SW | - | |
|
||||||
|
| note | 비고 | TXT | - | |
|
||||||
|
|
||||||
|
창고
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| warehouse_code | 창고코드 | TXT | 필수, 고유 | 수정 RO |
|
||||||
|
| warehouse_name | 창고명 | TXT | 필수 | |
|
||||||
|
| zipcode | 우편번호 | TXT | 필수 | 검색 모달로 채움 |
|
||||||
|
| address_detail | 상세주소 | TXT | 필수 | |
|
||||||
|
| is_active | 사용여부 | SW | - | |
|
||||||
|
| note | 비고 | TXT | - | |
|
||||||
|
|
||||||
|
고객사
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| customer_code | 고객사코드 | TXT | 필수, 고유 | 수정 RO |
|
||||||
|
| customer_name | 고객사명 | TXT | 필수 | |
|
||||||
|
| types | 유형 | DD(멀티) | 최소 1 | 파트너/일반 |
|
||||||
|
| email | 이메일 | TXT | 이메일형식 | |
|
||||||
|
| phone | 연락처 | TXT | - | |
|
||||||
|
| zipcode | 우편번호 | TXT | - | 검색 모달 |
|
||||||
|
| address_detail | 상세주소 | TXT | - | |
|
||||||
|
| is_active | 사용여부 | SW | - | |
|
||||||
|
| note | 비고 | TXT | - | |
|
||||||
|
|
||||||
|
사용자(사원)
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| employee_no | 사번 | TXT | 필수, 고유 | 수정 RO |
|
||||||
|
| employee_name | 성명 | TXT | 필수 | |
|
||||||
|
| email | 이메일 | TXT | 이메일형식 | |
|
||||||
|
| phone | 연락처 | TXT | - | |
|
||||||
|
| group_id | 그룹 | DD | 필수 | `/groups` |
|
||||||
|
| is_active | 사용여부 | SW | - | |
|
||||||
|
| note | 비고 | TXT | - | |
|
||||||
|
|
||||||
|
그룹
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| group_name | 그룹명 | TXT | 필수, 고유 | 수정 RO |
|
||||||
|
| description | 설명 | TXT | - | |
|
||||||
|
| is_default | 기본여부 | SW | - | |
|
||||||
|
| is_active | 사용여부 | SW | - | |
|
||||||
|
| note | 비고 | TXT | - | |
|
||||||
|
|
||||||
|
메뉴
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| menu_code | 메뉴코드 | TXT | 필수, 고유 | 수정 RO |
|
||||||
|
| menu_name | 메뉴명 | TXT | 필수 | |
|
||||||
|
| parent_id | 상위메뉴 | DD | - | `/menus` |
|
||||||
|
| route_path | 경로 | TXT | 필수 | |
|
||||||
|
| display_order | 표시순서 | NUM | >=0 | |
|
||||||
|
| is_active | 사용여부 | SW | - | |
|
||||||
|
| note | 비고 | TXT | - | |
|
||||||
|
|
||||||
|
그룹 메뉴 권한
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| group_id | 그룹 | DD | 필수 | RO(수정) |
|
||||||
|
| menu_id | 메뉴 | DD | 필수 | RO(수정) |
|
||||||
|
| can_create | 생성 | CHK | - | |
|
||||||
|
| can_read | 조회 | CHK | - | |
|
||||||
|
| can_update | 수정 | CHK | - | |
|
||||||
|
| can_delete | 삭제 | CHK | - | |
|
||||||
|
| is_active | 사용여부 | SW | - | |
|
||||||
|
|
||||||
|
결재
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| transaction_id | 트랜잭션번호 | DD | 필수 | |
|
||||||
|
| approval_no | 결재번호 | RO | - | 자동 부여 |
|
||||||
|
| approval_status_id | 상태 | DD | 필수 | 기본=대기 |
|
||||||
|
| requested_by_id | 상신자 | RO | - | 로그인 사용자 |
|
||||||
|
| requested_at | 요청일시 | RO | - | 자동 |
|
||||||
|
| note | 비고 | TXT | - | |
|
||||||
|
|
||||||
|
결재 단계
|
||||||
|
| key | 라벨 | 유형 | 검증 | 비고 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| approval_id | 결재ID | DD | 필수 | 수정 RO |
|
||||||
|
| step_order | 순서 | NUM | 필수, >=1 | 수정 RO |
|
||||||
|
| approver_id | 승인자 | DD | 필수 | `/employees` |
|
||||||
|
| step_status_id | 상태 | DD | 필수 | `/approval-statuses` |
|
||||||
|
| assigned_at | 배정일시 | RO | - | 자동 |
|
||||||
|
| decided_at | 결정일시 | RO | - | 자동 |
|
||||||
|
| note | 비고 | TXT | - | |
|
||||||
|
|
||||||
|
결재 이력: 조회 전용(테이블 컬럼 정의만, 입력/수정 없음).
|
||||||
|
|
||||||
|
결재 템플릿: 헤더(코드/명/설명/작성자/사용여부/비고) + 단계(순서/승인자 반복).
|
||||||
|
|
||||||
|
우편번호 검색: 입력(검색어) → 결과 테이블에서 선택 시 부모의 `zipcode`/주소 구성요소 세팅.
|
||||||
|
|
||||||
|
## 12. 테이블 스펙(열/정렬/반응형)
|
||||||
|
|
||||||
|
공통: 정렬은 기본 `updated_at desc` 또는 업무상 자연스러운 키, 헤더 클릭으로 1열 정렬 전환. 폭(px)은 힌트 값이며 브레이크포인트에 따라 가변.
|
||||||
|
|
||||||
|
입고 목록
|
||||||
|
- 컬럼: 번호(80, center, sortable=id) | 처리일자(140, center, sortable) | 창고(160) | 트랜잭션번호(180) | 상태(120, center) | 작성자(140) | 품목수(100, right) | 총수량(120, right) | 비고(1fr)
|
||||||
|
- 모바일(<768): 번호, 처리일자, 상태, 품목수만 노출. 확장 시 나머지 표시.
|
||||||
|
|
||||||
|
출고 목록
|
||||||
|
- 컬럼: 번호 | 처리일자 | 창고 | 트랜잭션번호 | 상태 | 작성자 | 고객수(100, right) | 품목수(100, right) | 총수량(120, right) | 비고(1fr)
|
||||||
|
- 모바일: 번호/처리일자/상태/고객수.
|
||||||
|
|
||||||
|
대여 목록
|
||||||
|
- 컬럼: 번호 | 처리일자 | 창고 | 대여/반납(120, center) | 트랜잭션번호 | 상태 | 반납예정일(160, center) | 고객수 | 품목수 | 비고
|
||||||
|
- 모바일: 번호/대여구분/반납예정일/상태.
|
||||||
|
|
||||||
|
마스터 테이블(벤더/제품/창고/고객/사용자/그룹/메뉴/권한)과 결재/단계/이력/템플릿은 `doc/입출고 대여 폼 정리.md`의 1행 예시를 기준으로 동일한 컬럼 구성, 모바일에서는 코드/이름/상태 위주 노출.
|
||||||
|
|
||||||
|
열 가시성 프리셋(예시)
|
||||||
|
- 데스크톱(≥1280): 모든 열 표시. 비고는 1fr로 확장.
|
||||||
|
- 태블릿(≥1024): 우선순위 낮은 열(비고/표시순서 등) 일부 숨김, 총합/상태/핵심 식별자 유지.
|
||||||
|
- 모바일(<768): 핵심 3~4열만 표시(예: 번호/상태/일자/구분). 나머지는 행 확장 또는 상세에서 제공.
|
||||||
|
|
||||||
|
## 13. 폼 레이아웃 & 와이어프레임(텍스트)
|
||||||
|
|
||||||
|
공통 모달(데스크톱)
|
||||||
|
```
|
||||||
|
[모달 헤더: 타이틀]
|
||||||
|
────────────────────────────────────────────
|
||||||
|
[필드 그리드 2~3열]
|
||||||
|
[라벨] [입력] [라벨] [입력] [라벨] [입력]
|
||||||
|
...
|
||||||
|
|
||||||
|
[섹션 타이틀] (예: 라인 품목)
|
||||||
|
[라인 테이블: 제품 | 제조사 | 단위 | 수량 | 단가 | 비고 | (+행)]
|
||||||
|
|
||||||
|
[하단 합계/요약 배지]
|
||||||
|
|
||||||
|
[푸터: 취소][저장]
|
||||||
|
```
|
||||||
|
|
||||||
|
모바일(풀스크린)
|
||||||
|
```
|
||||||
|
[<] 제목 [저장]
|
||||||
|
────────────────────────────────────────
|
||||||
|
[필드 1열 스택]
|
||||||
|
[섹션]
|
||||||
|
[라인: 카드형 반복]
|
||||||
|
[합계]
|
||||||
|
```
|
||||||
|
|
||||||
|
입고 등록 예시(데스크톱, 3열)
|
||||||
|
```
|
||||||
|
처리일자 [DT] 창고 [DD] 상태 [DD]
|
||||||
|
작성자 [RO] 비고 [TXT···(colspan=2)]
|
||||||
|
|
||||||
|
라인 섹션:
|
||||||
|
┌제품 [AA]┬제조사[RO]┬단위[RO]┬수량[NUM]┬단가[NUM]┬비고[TXT]┬(+)
|
||||||
|
└────────┴─────────┴──────┴───────┴───────┴───────┴────
|
||||||
|
|
||||||
|
[품목수: n][총수량: x][총금액: ₩y]
|
||||||
|
```
|
||||||
|
|
||||||
|
출고 등록: 상단 동일 + [고객사(멀티) 토큰 리스트 + (+추가) 자동완성]
|
||||||
|
|
||||||
|
## 14. 상호작용/플로우
|
||||||
|
- 리스트 → +신규 클릭 → 모달 → 저장 → 성공 토스트 → 리스트 리프레시/최상단 스크롤.
|
||||||
|
- 행 클릭 → 상세 모달(또는 우측 패널) → 수정 가능 상태면 “수정” 노출.
|
||||||
|
- 라인 편집: 수량/단가 변경 시 합계 즉시 반영. Enter/Tab으로 다음 셀 이동. (+)는 마지막 행 뒤에 포커스.
|
||||||
|
- 출고/대여 고객 토큰: 입력 자동완성 → Enter로 선택 → 토큰 생성. 토큰 X 클릭으로 제거.
|
||||||
|
- 삭제: 소프트 삭제 확인 모달 → 성공 시 상태 배지/행 스타일로 삭제 표시(필터로 숨김 기본).
|
||||||
|
|
||||||
|
## 15. 검증/오류 메시지(예시)
|
||||||
|
- 필수 누락: “처리일자를 입력해 주세요.”
|
||||||
|
- 수량 음수: “수량은 1 이상이어야 합니다.”
|
||||||
|
- 고객 미선택(출고/대여): “고객사는 최소 1건 선택해야 합니다.”
|
||||||
|
- 결재 상태 전이 금지: “현재 단계 상태에서는 다음 단계로 이동할 수 없습니다.”
|
||||||
|
- 네트워크 오류 시: 오류 토스트/다시 시도 버튼/오류 상세 제공(더미 데이터 사용 금지).
|
||||||
|
|
||||||
|
## 16. 키보드/접근성
|
||||||
|
- 단축키: Cmd/Ctrl+S 저장, Esc 닫기, Enter 다음 필드/행 추가(라인 끝).
|
||||||
|
- 포커스 순서: 위→아래, 좌→우. 포커스 트랩으로 모달 내에서 순환.
|
||||||
|
- ARIA 라벨 등 시맨틱: 가능한 범위에서 위젯 라벨/설명 제공.
|
||||||
|
|
||||||
|
## 17. 환경/피처 토글
|
||||||
|
- 환경 변수로 API 베이스 URL/타임아웃/로그 레벨 제어.
|
||||||
|
- 기능 플래그로 베타 화면/행동 토글(데이터는 항상 실 API 사용).
|
||||||
|
- 페이지네이션: 기본 20행, 최대 100행.
|
||||||
|
|
||||||
|
## 18. 상태 전이(결재)
|
||||||
|
- approval_statuses: 대기(기본, blocking), 진행중(blocking), 보류(blocking), 승인(non-blocking), 반려(terminal, blocking).
|
||||||
|
- 단계 행위: approve → 다음 단계 배정, 모든 단계 승인 시 전체 승인. reject → 전체 반려(terminal) 처리.
|
||||||
|
- 전이 금지: blocking 상태에서는 `can_proceed=false` 처리(버튼 비활성/툴팁).
|
||||||
|
|
||||||
|
## 19. API 매핑(연동 시)
|
||||||
|
- 트랜잭션 목록: `GET /stock-transactions?include=lines,customers`
|
||||||
|
- 생성: `POST /stock-transactions` 바디 내 헤더/라인/고객 배열 동시 전달
|
||||||
|
- 결재 상세: `GET /approvals/{id}?include=steps,histories`
|
||||||
|
- 단계 행위: `POST /approval-steps/{id}/actions` with `approval_action_id`
|
||||||
|
- 결재 템플릿: `GET/POST/PATCH /approval-templates`, `POST/PATCH /approval-templates/{id}/steps`
|
||||||
|
- 룩업: `/uoms`, `/transaction-types`, `/transaction-statuses`, `/approval-statuses`, `/approval-actions`
|
||||||
|
|
||||||
|
## 20. 컴포넌트 매핑(shadcn_ui)
|
||||||
|
- 입력: `ShadInput`, 숫자 입력은 우측 정렬 스타일.
|
||||||
|
- 선택: `ShadSelect`(단일), 자동완성은 `ShadSelect + 검색` 패턴 또는 커스텀 콤보(리스트 팝오버, 키보드 선택) — 레퍼런스: 슈퍼포트 구현체.
|
||||||
|
- 날짜: `SuperportShadDatePicker`, 기간 필터는 `SuperportShadDateRangePicker`.
|
||||||
|
- 스위치/체크: `ShadSwitch`, `ShadCheckbox`.
|
||||||
|
- 모달: `SuperportShadDialog`(헤더/본문/푸터), 모바일 풀스크린 전환.
|
||||||
|
- 테이블: `ShadTable.list` + `two_dimensional_scrollables`로 스크롤 최적화.
|
||||||
|
|
||||||
|
---
|
||||||
|
부록: 필요 시 각 스크린의 모바일 카드형 레이아웃 명세(요약 배지, 핵심 필드 순서)를 추가 정의한다.
|
||||||
955
doc/stock_approval_system_api_v4.md
Normal file
@@ -0,0 +1,955 @@
|
|||||||
|
# 간단 입·출고 + 결재 시스템 API 규격 (v4)
|
||||||
|
|
||||||
|
**기준 버전:** 2025-09-18 16:22:30Z (UTC)
|
||||||
|
|
||||||
|
본 문서는 `stock_approval_system_spec_full_v4.md`의 데이터 모델과 비즈니스 규칙을 기반으로 한 REST API 구성을 정의한다. 기본 CRUD를 제공하며, 목록·상세 조회 시 FK로 연결된 주요 엔터티 정보를 함께 반환한다. 모든 엔드포인트는 소프트 삭제 컬럼(`is_deleted`)을 노출하지 않는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 공통 규칙
|
||||||
|
- **URI 규칙:** 복수형 리소스 명 사용. 기본 경로 예) `/api/v1/vendors`.
|
||||||
|
- **표준 응답 구조:** 목록은 `{ items: [], page, page_size, total }`, 단건은 `{ data: { ... } }`.
|
||||||
|
- **시간대:** 모든 날짜·시간은 ISO8601 UTC 문자열.
|
||||||
|
- **소프트 삭제:** `DELETE /{res}/{id}` 호출 시 서버는 `is_deleted=true`, `is_active=false`로 처리하고 응답 바디는 `{ data: { id, deleted_at } }` 형식을 사용.
|
||||||
|
- **복구:** `POST /{res}/{id}/restore`.
|
||||||
|
- **공통 컬럼:** `note`, `is_active`, `created_at`, `updated_at`는 요청·응답에 필요 시 노출하되 `is_deleted`는 절대 노출하지 않는다.
|
||||||
|
- **기본 필터:** 목록 조회 시 기본 쿼리 `active=true`, `deleted=false`. `deleted` 파라미터가 `true`일 때에만 삭제된 항목을 반환.
|
||||||
|
- **증분 조회:** `updated_since=ISO8601`.
|
||||||
|
- **정렬:** `sort`(기본 `updated_at`), `order=asc|desc`(기본 desc).
|
||||||
|
- **검색:** `q` 파라미터로 코드/명칭 부분 일치. 필요한 경우 컬럼별 필터 지원.
|
||||||
|
- **Include 확장:** `include` 쿼리로 추가 데이터(`lines`, `customers`, `approval`, `steps`, `histories`, `permissions`, `employees` 등) 선택 가능. 포함 대상은 FK 요약 정보를 이미 반환하므로 `include`는 상세 컬렉션을 불러올 때 사용.
|
||||||
|
- **배열 입력:** 트랜잭션 라인, 트랜잭션 고객, 결재 단계, 그룹 메뉴 권한 등 다건 작업은 항상 배열(`[]`) 기반으로 요청한다.
|
||||||
|
- **Primary Key 규칙:** Create 요청 바디에는 PK를 포함하지 않는다. Create 응답 및 나머지 모든 요청·응답에는 PK가 포함돼야 한다(경로에 이미 포함된 경우라도 바디 내 `id`를 명시).
|
||||||
|
- **에러 규격:**
|
||||||
|
- `400 BAD_REQUEST` — 검증 오류, 필수값 누락.
|
||||||
|
- `404 NOT_FOUND` — 리소스 없음 또는 삭제됨.
|
||||||
|
- `409 CONFLICT` — 유니크 제약, 결재 단계 상태 충돌.
|
||||||
|
- `422 UNPROCESSABLE_ENTITY` — 비즈니스 규칙 위반(출고 고객 누락, blocking 상태 전이 등).
|
||||||
|
- 에러 응답 예: `{ "error": { "code": 422, "message": "출고 트랜잭션에는 고객이 최소 1건 필요합니다.", "details": [...] } }`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 타입(룩업) API
|
||||||
|
대상: `/uoms`, `/transaction-types`, `/transaction-statuses`, `/approval-statuses`, `/approval-actions`
|
||||||
|
|
||||||
|
### 2.1 목록 조회
|
||||||
|
`GET /{type}?page=1&page_size=50&active=true`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "EA",
|
||||||
|
"is_default": true,
|
||||||
|
"is_active": true,
|
||||||
|
"note": null,
|
||||||
|
"created_at": "2025-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-02-01T03:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 단건 조회
|
||||||
|
`GET /{type}/{id}`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 3,
|
||||||
|
"name": "반려",
|
||||||
|
"is_default": false,
|
||||||
|
"is_blocking_next": true,
|
||||||
|
"is_terminal": true,
|
||||||
|
"is_active": true,
|
||||||
|
"note": "최종 거절",
|
||||||
|
"created_at": "2025-01-10T09:00:00Z",
|
||||||
|
"updated_at": "2025-02-01T10:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 생성
|
||||||
|
`POST /{type}`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "진행중",
|
||||||
|
"is_default": false,
|
||||||
|
"is_blocking_next": true,
|
||||||
|
"is_terminal": false,
|
||||||
|
"is_active": true,
|
||||||
|
"note": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
응답:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 4,
|
||||||
|
"name": "진행중",
|
||||||
|
"is_default": false,
|
||||||
|
"is_blocking_next": true,
|
||||||
|
"is_terminal": false,
|
||||||
|
"is_active": true,
|
||||||
|
"note": null,
|
||||||
|
"created_at": "2025-03-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-03-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 수정
|
||||||
|
`PATCH /{type}/{id}`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"is_blocking_next": false,
|
||||||
|
"note": "임시 승인 허용"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 삭제 & 복구
|
||||||
|
- `DELETE /{type}/{id}` → `{ "data": { "id": 4, "deleted_at": "2025-03-05T09:00:00Z" } }`
|
||||||
|
- `POST /{type}/{id}/restore` → `{ "data": { "id": 4, "restored_at": "2025-03-06T01:00:00Z" } }`
|
||||||
|
|
||||||
|
> `approval-statuses`는 추가 속성(`is_blocking_next`, `is_terminal`)을 사용하며, 다른 타입 테이블은 `name`, `is_default`, `is_active`, `note` 중심으로 작동한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 마스터 데이터 API
|
||||||
|
리소스: `/vendors`, `/warehouses`, `/customers`, `/employees`, `/products`, `/menus`, `/groups`, `/zipcodes`
|
||||||
|
|
||||||
|
### 3.1 목록 조회
|
||||||
|
`GET /vendors?page=1&q=한빛`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"vendor_code": "V001",
|
||||||
|
"vendor_name": "한빛상사",
|
||||||
|
"note": "서울/경기 공급처",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-01-01T12:00:00Z",
|
||||||
|
"updated_at": "2025-01-03T09:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /products?page=1&include=vendor`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"product_code": "P100",
|
||||||
|
"product_name": "샘플",
|
||||||
|
"vendor": {
|
||||||
|
"id": 10,
|
||||||
|
"vendor_code": "V001",
|
||||||
|
"vendor_name": "한빛상사"
|
||||||
|
},
|
||||||
|
"uom": {
|
||||||
|
"id": 1,
|
||||||
|
"uom_name": "EA",
|
||||||
|
"is_default": true
|
||||||
|
},
|
||||||
|
"note": "출고 우선 재고",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-02-01T12:00:00Z",
|
||||||
|
"updated_at": "2025-02-03T09:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /warehouses?page=1`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"warehouse_code": "WH-001",
|
||||||
|
"warehouse_name": "1센터",
|
||||||
|
"zipcode": {
|
||||||
|
"zipcode": "06000",
|
||||||
|
"sido": "서울특별시",
|
||||||
|
"sigungu": "강남구",
|
||||||
|
"road_name": "테헤란로"
|
||||||
|
},
|
||||||
|
"address_detail": "강남파이낸스센터 10층",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-01-05T08:00:00Z",
|
||||||
|
"updated_at": "2025-01-10T09:30:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /customers?page=1`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 301,
|
||||||
|
"customer_code": "C001",
|
||||||
|
"customer_name": "ABC물류",
|
||||||
|
"is_partner": true,
|
||||||
|
"is_general": false,
|
||||||
|
"email": "contact@abc.com",
|
||||||
|
"mobile_no": "010-1234-5678",
|
||||||
|
"zipcode": {
|
||||||
|
"zipcode": "06000",
|
||||||
|
"sido": "서울특별시",
|
||||||
|
"sigungu": "강남구",
|
||||||
|
"road_name": "테헤란로"
|
||||||
|
},
|
||||||
|
"address_detail": "10층",
|
||||||
|
"note": "VIP 고객",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-01-15T11:00:00Z",
|
||||||
|
"updated_at": "2025-01-20T08:10:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /employees?page=1`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"employee_no": "E2025001",
|
||||||
|
"employee_name": "김승인",
|
||||||
|
"email": "approver@example.com",
|
||||||
|
"mobile_no": "010-2222-1111",
|
||||||
|
"group": {
|
||||||
|
"id": 2,
|
||||||
|
"group_name": "창고 관리자"
|
||||||
|
},
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-01-02T09:00:00Z",
|
||||||
|
"updated_at": "2025-01-10T11:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /groups?include=permissions,employees`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"group_name": "창고 관리자",
|
||||||
|
"group_description": "창고 및 재고 관리",
|
||||||
|
"is_default": false,
|
||||||
|
"is_active": true,
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"menu": {
|
||||||
|
"id": 12,
|
||||||
|
"menu_code": "STOCK_MGMT",
|
||||||
|
"menu_name": "입출고 관리"
|
||||||
|
},
|
||||||
|
"can_create": true,
|
||||||
|
"can_read": true,
|
||||||
|
"can_update": true,
|
||||||
|
"can_delete": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"employees": [
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"employee_no": "E2025001",
|
||||||
|
"employee_name": "김승인"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created_at": "2025-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-15T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 단건 조회
|
||||||
|
`GET /products/101`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 101,
|
||||||
|
"product_code": "P100",
|
||||||
|
"product_name": "샘플",
|
||||||
|
"vendor": {
|
||||||
|
"id": 10,
|
||||||
|
"vendor_code": "V001",
|
||||||
|
"vendor_name": "한빛상사",
|
||||||
|
"note": "서울/경기 공급처"
|
||||||
|
},
|
||||||
|
"uom": {
|
||||||
|
"id": 1,
|
||||||
|
"uom_name": "EA",
|
||||||
|
"is_default": true
|
||||||
|
},
|
||||||
|
"note": "출고 우선 재고",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-02-01T12:00:00Z",
|
||||||
|
"updated_at": "2025-02-03T09:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 생성
|
||||||
|
`POST /vendors`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vendor_code": "V002",
|
||||||
|
"vendor_name": "미래상사",
|
||||||
|
"note": "부산 공급처",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
응답:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 11,
|
||||||
|
"vendor_code": "V002",
|
||||||
|
"vendor_name": "미래상사",
|
||||||
|
"note": "부산 공급처",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-03-01T00:00:00Z",
|
||||||
|
"updated_at": "2025-03-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 수정
|
||||||
|
`PATCH /products/101`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"product_name": "샘플 A",
|
||||||
|
"note": "재고 우선순위 변경"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 삭제 & 복구
|
||||||
|
- `DELETE /products/101`
|
||||||
|
- `POST /products/101/restore`
|
||||||
|
|
||||||
|
### 3.6 그룹 메뉴 권한 일괄 갱신
|
||||||
|
`POST /groups/2/permissions`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"menu_id": 12,
|
||||||
|
"can_create": true,
|
||||||
|
"can_read": true,
|
||||||
|
"can_update": true,
|
||||||
|
"can_delete": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"menu_id": 13,
|
||||||
|
"can_create": false,
|
||||||
|
"can_read": true,
|
||||||
|
"can_update": false,
|
||||||
|
"can_delete": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
응답은 갱신된 권한 목록을 반환.
|
||||||
|
|
||||||
|
> `zipcodes`는 대량 데이터 특성상 `GET /zipcodes?zipcode=06000&road_name=세종대로` 형태로 조회하며, 응답 항목에는 `zipcode`, `sido`, `sigungu`, `road_name`, `building_main_no` 등 주소 구성 요소가 포함된다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 트랜잭션 API
|
||||||
|
리소스: `/stock-transactions`, 보조 리소스: `/transaction-lines`, `/transaction-customers`
|
||||||
|
|
||||||
|
### 4.1 생성 (헤더 + 라인 + 고객 다건)
|
||||||
|
`POST /stock-transactions`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"transaction_no": "TXN-2025-0001",
|
||||||
|
"transaction_type_id": 1,
|
||||||
|
"transaction_status_id": 1,
|
||||||
|
"warehouse_id": 1,
|
||||||
|
"transaction_date": "2025-09-18",
|
||||||
|
"created_by_id": 7,
|
||||||
|
"note": "창고 입고",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"line_no": 1,
|
||||||
|
"product_id": 101,
|
||||||
|
"quantity": 50,
|
||||||
|
"unit_price": 1200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"line_no": 2,
|
||||||
|
"product_id": 102,
|
||||||
|
"quantity": 20,
|
||||||
|
"unit_price": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"customers": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
응답은 생성된 트랜잭션 전체 정보를 반환하며, 라인·고객 식별자가 포함된다.
|
||||||
|
|
||||||
|
### 4.2 목록 조회
|
||||||
|
`GET /stock-transactions?include=lines,customers,approval`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 9001,
|
||||||
|
"transaction_no": "TXN-2025-0001",
|
||||||
|
"transaction_type": {
|
||||||
|
"id": 1,
|
||||||
|
"type_name": "입고"
|
||||||
|
},
|
||||||
|
"transaction_status": {
|
||||||
|
"id": 1,
|
||||||
|
"status_name": "초안"
|
||||||
|
},
|
||||||
|
"warehouse": {
|
||||||
|
"id": 1,
|
||||||
|
"warehouse_code": "WH-001",
|
||||||
|
"warehouse_name": "1센터"
|
||||||
|
},
|
||||||
|
"transaction_date": "2025-09-18",
|
||||||
|
"created_by": {
|
||||||
|
"id": 7,
|
||||||
|
"employee_no": "E2025001",
|
||||||
|
"employee_name": "김승인"
|
||||||
|
},
|
||||||
|
"note": "창고 입고",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-09-18T05:00:00Z",
|
||||||
|
"updated_at": "2025-09-18T05:00:00Z",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"id": 12001,
|
||||||
|
"line_no": 1,
|
||||||
|
"product": {
|
||||||
|
"id": 101,
|
||||||
|
"product_code": "P100",
|
||||||
|
"product_name": "샘플",
|
||||||
|
"vendor": {
|
||||||
|
"id": 10,
|
||||||
|
"vendor_name": "한빛상사"
|
||||||
|
},
|
||||||
|
"uom": {
|
||||||
|
"id": 1,
|
||||||
|
"uom_name": "EA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quantity": 50,
|
||||||
|
"unit_price": 1200,
|
||||||
|
"note": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"customers": [],
|
||||||
|
"approval": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 단건 조회
|
||||||
|
`GET /stock-transactions/9001?include=lines,customers,approval,approval.steps`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 9001,
|
||||||
|
"transaction_no": "TXN-2025-0001",
|
||||||
|
"transaction_type": {
|
||||||
|
"id": 1,
|
||||||
|
"type_name": "입고"
|
||||||
|
},
|
||||||
|
"transaction_status": {
|
||||||
|
"id": 1,
|
||||||
|
"status_name": "초안"
|
||||||
|
},
|
||||||
|
"warehouse": {
|
||||||
|
"id": 1,
|
||||||
|
"warehouse_code": "WH-001",
|
||||||
|
"warehouse_name": "1센터",
|
||||||
|
"zipcode": {
|
||||||
|
"zipcode": "06000",
|
||||||
|
"sido": "서울특별시"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transaction_date": "2025-09-18",
|
||||||
|
"created_by": {
|
||||||
|
"id": 7,
|
||||||
|
"employee_no": "E2025001",
|
||||||
|
"employee_name": "김승인"
|
||||||
|
},
|
||||||
|
"note": "창고 입고",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-09-18T05:00:00Z",
|
||||||
|
"updated_at": "2025-09-18T05:00:00Z",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"id": 12001,
|
||||||
|
"line_no": 1,
|
||||||
|
"product": {
|
||||||
|
"id": 101,
|
||||||
|
"product_code": "P100",
|
||||||
|
"product_name": "샘플",
|
||||||
|
"vendor": {
|
||||||
|
"id": 10,
|
||||||
|
"vendor_name": "한빛상사"
|
||||||
|
},
|
||||||
|
"uom": {
|
||||||
|
"id": 1,
|
||||||
|
"uom_name": "EA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quantity": 50,
|
||||||
|
"unit_price": 1200,
|
||||||
|
"note": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"customers": [],
|
||||||
|
"approval": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 헤더 수정
|
||||||
|
`PATCH /stock-transactions/9001`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 9001,
|
||||||
|
"transaction_status_id": 2,
|
||||||
|
"note": "상신 준비"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 라인 다건 추가/수정/삭제
|
||||||
|
- **추가:** `POST /stock-transactions/9001/lines`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 9001,
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"line_no": 2,
|
||||||
|
"product_id": 102,
|
||||||
|
"quantity": 20,
|
||||||
|
"unit_price": 900
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **일괄 수정:** `PATCH /stock-transactions/9001/lines`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 9001,
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"id": 12001,
|
||||||
|
"line_no": 1,
|
||||||
|
"quantity": 60,
|
||||||
|
"note": "추가 입고"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12002,
|
||||||
|
"line_no": 2,
|
||||||
|
"unit_price": 950
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **삭제:** `DELETE /transaction-lines/12002`
|
||||||
|
- **복구:** `POST /transaction-lines/12002/restore`
|
||||||
|
|
||||||
|
### 4.6 고객 연결 다건 관리
|
||||||
|
- **추가:** `POST /stock-transactions/9100/customers`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 9100,
|
||||||
|
"customers": [
|
||||||
|
{
|
||||||
|
"customer_id": 301,
|
||||||
|
"note": "1차 납품"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customer_id": 302,
|
||||||
|
"note": "2차 납품"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **수정:** `PATCH /stock-transactions/9100/customers`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 9100,
|
||||||
|
"customers": [
|
||||||
|
{
|
||||||
|
"id": 33001,
|
||||||
|
"note": "수량 조정"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **삭제:** `DELETE /transaction-customers/33001`
|
||||||
|
|
||||||
|
### 4.7 상태 전이 권장 API
|
||||||
|
- `POST /stock-transactions/9001/submit`
|
||||||
|
- `POST /stock-transactions/9001/complete`
|
||||||
|
|
||||||
|
응답은 `{ "data": { "id": 9001, "transaction_status": { ... }, "updated_at": "..." } }` 형태.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 결재 API
|
||||||
|
리소스: `/approvals`, 보조 리소스: `/approval-steps`, `/approval-histories`
|
||||||
|
|
||||||
|
### 5.1 결재 생성
|
||||||
|
`POST /approvals`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"transaction_id": 9001,
|
||||||
|
"approval_no": "APP-2025-0001",
|
||||||
|
"approval_status_id": 1,
|
||||||
|
"requested_by_id": 7,
|
||||||
|
"note": "입고 결재"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
응답에는 `id`와 현재 단계 정보가 포함된다.
|
||||||
|
|
||||||
|
### 5.2 목록 조회
|
||||||
|
`GET /approvals?include=steps,histories`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 5001,
|
||||||
|
"approval_no": "APP-2025-0001",
|
||||||
|
"transaction": {
|
||||||
|
"id": 9001,
|
||||||
|
"transaction_no": "TXN-2025-0001"
|
||||||
|
},
|
||||||
|
"approval_status": {
|
||||||
|
"id": 1,
|
||||||
|
"status_name": "대기",
|
||||||
|
"is_blocking_next": true
|
||||||
|
},
|
||||||
|
"current_step": null,
|
||||||
|
"requested_by": {
|
||||||
|
"id": 7,
|
||||||
|
"employee_no": "E2025001",
|
||||||
|
"employee_name": "김승인"
|
||||||
|
},
|
||||||
|
"requested_at": "2025-09-18T06:00:00Z",
|
||||||
|
"decided_at": null,
|
||||||
|
"note": "입고 결재",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-09-18T06:00:00Z",
|
||||||
|
"updated_at": "2025-09-18T06:00:00Z",
|
||||||
|
"steps": [],
|
||||||
|
"histories": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 단건 조회
|
||||||
|
`GET /approvals/5001?include=steps,histories`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 5001,
|
||||||
|
"approval_no": "APP-2025-0001",
|
||||||
|
"transaction": {
|
||||||
|
"id": 9001,
|
||||||
|
"transaction_no": "TXN-2025-0001"
|
||||||
|
},
|
||||||
|
"approval_status": {
|
||||||
|
"id": 1,
|
||||||
|
"status_name": "대기",
|
||||||
|
"is_blocking_next": true,
|
||||||
|
"is_terminal": false
|
||||||
|
},
|
||||||
|
"current_step": null,
|
||||||
|
"requested_by": {
|
||||||
|
"id": 7,
|
||||||
|
"employee_no": "E2025001",
|
||||||
|
"employee_name": "김승인"
|
||||||
|
},
|
||||||
|
"requested_at": "2025-09-18T06:00:00Z",
|
||||||
|
"decided_at": null,
|
||||||
|
"note": "입고 결재",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": 7001,
|
||||||
|
"step_order": 1,
|
||||||
|
"approver": {
|
||||||
|
"id": 21,
|
||||||
|
"employee_no": "E2025002",
|
||||||
|
"employee_name": "박검토"
|
||||||
|
},
|
||||||
|
"step_status": {
|
||||||
|
"id": 1,
|
||||||
|
"status_name": "대기",
|
||||||
|
"is_blocking_next": true
|
||||||
|
},
|
||||||
|
"assigned_at": "2025-09-18T06:05:00Z",
|
||||||
|
"decided_at": null,
|
||||||
|
"note": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"histories": [],
|
||||||
|
"created_at": "2025-09-18T06:00:00Z",
|
||||||
|
"updated_at": "2025-09-18T06:05:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 단계 구성 (배치 생성)
|
||||||
|
`POST /approvals/5001/steps`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 5001,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_order": 1,
|
||||||
|
"approver_id": 21,
|
||||||
|
"note": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_order": 2,
|
||||||
|
"approver_id": 34,
|
||||||
|
"note": "재무 확인"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 단계 일괄 수정/재배치
|
||||||
|
`PATCH /approvals/5001/steps`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 5001,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": 7001,
|
||||||
|
"step_order": 1,
|
||||||
|
"note": "서류 확인 중"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7002,
|
||||||
|
"step_order": 2,
|
||||||
|
"approver_id": 35
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 단계 행위
|
||||||
|
`POST /approval-steps/7001/actions`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 7001,
|
||||||
|
"approval_action_id": 1,
|
||||||
|
"note": "승인합니다."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
응답에는 전후 상태(`from_status`, `to_status`), 차기 단계 정보가 포함되며, `approval_histories`에 기록된다.
|
||||||
|
|
||||||
|
### 5.7 결재 상태 확인
|
||||||
|
`GET /approvals/5001/can-proceed`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 5001,
|
||||||
|
"can_proceed": true,
|
||||||
|
"reason": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.8 결재 수정·삭제·복구
|
||||||
|
- `PATCH /approvals/5001`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 5001,
|
||||||
|
"approval_status_id": 2,
|
||||||
|
"note": "보류 처리"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `DELETE /approvals/5001`
|
||||||
|
- `POST /approvals/5001/restore`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 결재 템플릿 API
|
||||||
|
리소스: `/approval-templates`
|
||||||
|
|
||||||
|
### 6.1 목록 조회
|
||||||
|
`GET /approval-templates?page=1`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 3001,
|
||||||
|
"template_code": "AP_INBOUND",
|
||||||
|
"template_name": "입고 결재 기본",
|
||||||
|
"description": "입고 결재 2단계",
|
||||||
|
"created_by": {
|
||||||
|
"id": 7,
|
||||||
|
"employee_no": "E2025001",
|
||||||
|
"employee_name": "김승인"
|
||||||
|
},
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-01-20T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-25T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 50,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 단건 조회
|
||||||
|
`GET /approval-templates/3001?include=steps`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 3001,
|
||||||
|
"template_code": "AP_INBOUND",
|
||||||
|
"template_name": "입고 결재 기본",
|
||||||
|
"description": "입고 결재 2단계",
|
||||||
|
"created_by": {
|
||||||
|
"id": 7,
|
||||||
|
"employee_no": "E2025001",
|
||||||
|
"employee_name": "김승인"
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": 9101,
|
||||||
|
"step_order": 1,
|
||||||
|
"approver": {
|
||||||
|
"id": 21,
|
||||||
|
"employee_no": "E2025002",
|
||||||
|
"employee_name": "박검토"
|
||||||
|
},
|
||||||
|
"note": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-01-20T00:00:00Z",
|
||||||
|
"updated_at": "2025-01-25T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 생성·수정
|
||||||
|
- `POST /approval-templates`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"template_code": "AP_OUTBOUND",
|
||||||
|
"template_name": "출고 결재 기본",
|
||||||
|
"description": "출고 결재 3단계",
|
||||||
|
"created_by_id": 7,
|
||||||
|
"note": "표준 출고"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `POST /approval-templates/3002/steps`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 3002,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_order": 1,
|
||||||
|
"approver_id": 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_order": 2,
|
||||||
|
"approver_id": 55
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `PATCH /approval-templates/3002`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 3002,
|
||||||
|
"template_name": "출고 결재 확장",
|
||||||
|
"note": "정기 출고용"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `PATCH /approval-templates/3002/steps`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 3002,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": 9105,
|
||||||
|
"step_order": 1,
|
||||||
|
"approver_id": 36
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 삭제/복구: `DELETE /approval-templates/{id}`, `POST /approval-templates/{id}/restore`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 보고서 API (선택)
|
||||||
|
- `GET /reports/transactions/export?from=2025-09-01&to=2025-09-30&type_id=2&warehouse_id=1&format=xlsx`
|
||||||
|
- `GET /reports/approvals/export?status_id=1&format=pdf`
|
||||||
|
|
||||||
|
응답은 파일 다운로드 링크 또는 스트림. 요청 파라미터에는 대상 리소스의 PK를 포함한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 구현 참고
|
||||||
|
- FK 요약 정보는 기본 응답에 포함하며, 상세 정보가 필요하면 `include` 파라미터를 활용해 확장한다.
|
||||||
|
- 배열 기반 다건 작업은 전체를 트랜잭션 처리해야 한다. 실패 시 롤백하고 부분 처리 결과를 반환하지 않는다.
|
||||||
|
- `is_active` 변경은 권한·결재 등의 즉시성 요구를 고려하여 관련 캐시를 무효화한다.
|
||||||
|
- 결재 단계 상태 전이는 `approval_statuses.is_blocking_next` 규칙을 준수해야 하며, 반려(`is_terminal=true`) 상태 시 결재를 종료한다.
|
||||||
677
doc/stock_approval_system_spec_v4.md
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
|
||||||
|
# 간단 입·출고 + 결재 시스템 설계서 (최종 v4)
|
||||||
|
|
||||||
|
**버전:** 2025-09-18 16:22:30Z (UTC)
|
||||||
|
**요약:** 벤더 ↔ 창고 ↔ 고객사 간 물품 이동(입고/출고)을 관리하는 최소구성 시스템.
|
||||||
|
- 트랜잭션당 1개의 결재(1:1), **승인자 순서 기반의 순차 결재** 지원.
|
||||||
|
- **다음 승인자로 넘어가면 안 되는 상태**를 `approval_statuses.is_blocking_next`로 제어.
|
||||||
|
- 모든 테이블(타입/코드 테이블 포함)에 **공통 컬럼** 적용: `is_active`, `is_deleted`, `created_at`, `updated_at`.
|
||||||
|
- **벤더는 트랜잭션 헤더에 연결하지 않음**(벤더는 제품을 통해서만 추적).
|
||||||
|
- **customer_roles 제거**: 트랜잭션-고객은 역할 없이 다수 연결만 허용.
|
||||||
|
- 타입값은 **별도 테이블**로 분리하며 `*_code`/정렬순서 미사용, ID 기반 참조.
|
||||||
|
- 메뉴 접근은 `groups`와 `group_menu_permissions`를 통해 제어되며, 모든 직원은 정확히 하나의 그룹에 속함.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0) 핵심 비즈니스 규칙
|
||||||
|
- 제품 1개는 반드시 1개의 벤더에 소속 (`products.vendor_id` 필수).
|
||||||
|
- **트랜잭션 1건당 결재 1건**(1:1, 소프트삭제 제외).
|
||||||
|
- 결재는 **승인자 순서(`approval_steps.step_order`)대로**만 진행.
|
||||||
|
- 각 단계 상태가 **blocking**이면 다음 단계로 이동 불가.
|
||||||
|
- 트랜잭션에는 **여러 고객사**를 연결할 수 있음(역할 없음).
|
||||||
|
- 모든 직원은 **그룹**에 속하며(`employees.group_id`), 그룹-메뉴 권한(`group_menu_permissions`)으로 메뉴별 CRUD 가능 여부가 결정됨.
|
||||||
|
- 고객사는 **유형**을 `is_partner`/`is_general` 플래그로 구분하며 둘 중 하나 이상이 true여야 함(기본: 일반 true, 파트너 false).
|
||||||
|
- 반복되는 결재 라인은 **결재 템플릿**으로 저장 후 호출하여 재사용 가능.
|
||||||
|
- 모든 삭제는 **소프트 삭제**(`is_deleted=true`)이며, 삭제 시 `is_active=false`로 내림.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) 개념 ERD
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
vendors ||--o{ products : supplies
|
||||||
|
uoms ||--o{ products : measured_in
|
||||||
|
|
||||||
|
warehouses ||--o{ stock_transactions : occurs_in
|
||||||
|
transaction_types ||--o{ stock_transactions : typed_as
|
||||||
|
transaction_statuses ||--o{ stock_transactions : has_status
|
||||||
|
|
||||||
|
stock_transactions ||--o{ transaction_lines : has
|
||||||
|
products ||--o{ transaction_lines : item
|
||||||
|
|
||||||
|
stock_transactions ||--o{ transaction_customers : serves
|
||||||
|
customers ||--o{ transaction_customers : party
|
||||||
|
|
||||||
|
stock_transactions ||--|| approvals : has_one
|
||||||
|
approval_statuses ||--o{ approvals : overall_status
|
||||||
|
approvals ||--o{ approval_steps : has_sequence
|
||||||
|
approval_statuses ||--o{ approval_steps : step_status
|
||||||
|
approval_steps ||--o{ approval_histories : logs
|
||||||
|
approval_actions ||--o{ approval_histories : acted_as
|
||||||
|
|
||||||
|
approval_templates ||--o{ approval_template_steps : has_sequence
|
||||||
|
|
||||||
|
employees ||--o{ approvals : requested_by
|
||||||
|
employees ||--o{ approval_steps : assigned_to
|
||||||
|
employees ||--o{ approval_histories : actor
|
||||||
|
employees ||--o{ stock_transactions : created_by
|
||||||
|
employees ||--o{ approval_templates : authored
|
||||||
|
employees ||--o{ approval_template_steps : template_approver
|
||||||
|
groups ||--o{ employees : members
|
||||||
|
groups ||--o{ group_menu_permissions : controls
|
||||||
|
menus ||--o{ group_menu_permissions : target
|
||||||
|
zipcodes ||--o{ warehouses : located
|
||||||
|
zipcodes ||--o{ customers : addressed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) 공통 컬럼 (모든 테이블 공통 적용)
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| note | 비고 | text | - | - | N | N | N | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | N | N | - |
|
||||||
|
| is_deleted | 삭제여부(소프트) | boolean | - | false | Y | N | N | - |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | N | N | - |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | N | N | - |
|
||||||
|
|
||||||
|
> 모든 테이블(타입/코드 테이블 포함)에 위 4개 컬럼을 **명시적으로 포함**.
|
||||||
|
> `note`는 테이블별 메모/추가 설명을 저장하는 자유 텍스트 필드.
|
||||||
|
> `updated_at`은 UPDATE 시 자동 갱신(트리거/생성 컬럼 권장). 삭제 시 `is_deleted=true`, `is_active=false` 처리.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) 테이블 정의
|
||||||
|
|
||||||
|
### 3.1 `vendors` (벤더)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| vendors | 벤더 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 벤더ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| vendor_code | 벤더코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| vendor_name | 벤더명 | varchar | 100 | - | Y | | | |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 `warehouses` (창고)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| warehouses | 창고 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 창고ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| warehouse_code | 창고코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| warehouse_name | 창고명 | varchar | 100 | - | Y | | | |
|
||||||
|
| zipcode | 우편번호 | varchar | 5 | - | N | | | zipcodes.zipcode |
|
||||||
|
| address_detail | 상세주소 | varchar | 200 | - | N | | | - |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 `customers` (고객사)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| customers | 고객사 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 고객사ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| customer_code | 고객사코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| customer_name | 고객사명 | varchar | 100 | - | Y | | | |
|
||||||
|
| is_partner | 파트너여부 | boolean | - | false | Y | | | - |
|
||||||
|
| is_general | 일반여부 | boolean | - | true | Y | | | - |
|
||||||
|
| email | 이메일 | varchar | 100 | - | N | | | - |
|
||||||
|
| mobile_no | 모바일번호 | varchar | 20 | - | N | | | - |
|
||||||
|
| zipcode | 우편번호 | varchar | 5 | - | N | | | zipcodes.zipcode |
|
||||||
|
| address_detail | 상세주소 | varchar | 200 | - | N | | | - |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 `employees` (사원)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| employees | 사원 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 사원ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| employee_no | 사번 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| employee_name | 성명 | varchar | 100 | - | Y | | | |
|
||||||
|
| email | 이메일 | varchar | 100 | - | N | Y | | |
|
||||||
|
| mobile_no | 모바일번호 | varchar | 20 | - | N | | | |
|
||||||
|
| group_id | 그룹ID | bigint | - | - | Y | | | groups.id |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 `zipcodes` (우편번호)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| zipcodes | 우편번호 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| zipcode | 우편번호 | varchar | 5 | - | Y | Y | Y | - |
|
||||||
|
| sido | 시도 | varchar | 50 | - | Y | | | - |
|
||||||
|
| sido_eng | 시도영문 | varchar | 100 | - | N | | | - |
|
||||||
|
| sigungu | 시군구 | varchar | 100 | - | Y | | | - |
|
||||||
|
| sigungu_eng | 시군구영문 | varchar | 100 | - | N | | | - |
|
||||||
|
| eupmyeon | 읍면 | varchar | 100 | - | N | | | - |
|
||||||
|
| eupmyeon_eng | 읍면영문 | varchar | 100 | - | N | | | - |
|
||||||
|
| road_code | 도로명코드 | varchar | 12 | - | Y | | | - |
|
||||||
|
| road_name | 도로명 | varchar | 200 | - | Y | | | - |
|
||||||
|
| road_name_eng | 도로명영문 | varchar | 200 | - | N | | | - |
|
||||||
|
| underground_flag | 지하여부 | varchar | 1 | 'N' | Y | | | - |
|
||||||
|
| building_main_no | 건물번호본번 | integer | - | 0 | Y | | | - |
|
||||||
|
| building_sub_no | 건물번호부번 | integer | - | 0 | N | | | - |
|
||||||
|
| building_mgmt_no | 건물관리번호 | varchar | 25 | - | Y | | | - |
|
||||||
|
| bulk_receiver | 다량배달처명 | varchar | 200 | - | N | | | - |
|
||||||
|
| sigungu_building_name | 시군구용건물명 | varchar | 200 | - | N | | | - |
|
||||||
|
| legal_dong_code | 법정동코드 | varchar | 10 | - | Y | | | - |
|
||||||
|
| legal_dong_name | 법정동명 | varchar | 100 | - | Y | | | - |
|
||||||
|
| ri_name | 리명 | varchar | 100 | - | N | | | - |
|
||||||
|
| admin_dong_name | 행정동명 | varchar | 100 | - | N | | | - |
|
||||||
|
| mountain_flag | 산여부 | varchar | 1 | 'N' | Y | | | - |
|
||||||
|
| land_main_no | 지번본번 | integer | - | 0 | Y | | | - |
|
||||||
|
| town_serial_no | 읍면동일련번호 | integer | - | 0 | N | | | - |
|
||||||
|
| land_sub_no | 지번부번 | integer | - | 0 | N | | | - |
|
||||||
|
| old_zipcode | 구우편번호 | varchar | 6 | - | N | | | - |
|
||||||
|
| zipcode_serial_no | 우편번호일련번호 | integer | - | 0 | Y | | | - |
|
||||||
|
| search_text | 검색텍스트 | text | - | - | N | | | - |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 도로명 주소 데이터와 매핑되는 5자리 우편번호 기준. `zipcode`가 PK이며 외부 데이터 동기화를 위한 `zipcode_serial_no`를 포함.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 `menus` (메뉴)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| menus | 메뉴 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 메뉴ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| menu_code | 메뉴코드 | varchar | 50 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| menu_name | 메뉴명 | varchar | 100 | - | Y | | | |
|
||||||
|
| parent_menu_id | 상위메뉴ID | bigint | - | - | N | | | menus.id |
|
||||||
|
| route_path | 경로 | varchar | 255 | - | N | | | - |
|
||||||
|
| display_order | 표시순서 | integer | - | 0 | Y | | | - |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 메뉴는 계층 구조를 지원하며 `parent_menu_id`가 NULL이면 1차 메뉴로 간주.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.8 `groups` (그룹)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| groups | 그룹 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 그룹ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| group_name | 그룹명 | varchar | 100 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| group_description | 그룹설명 | varchar | 255 | - | N | | | - |
|
||||||
|
| is_default | 기본그룹여부 | boolean | - | false | Y | | | |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> `group_menu_permissions`를 통해 각 그룹별 메뉴 CRUD 권한을 정의하며, 사원은 `employees.group_id`로 그룹에 연결됨.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.9 `group_menu_permissions` (그룹_메뉴_권한)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| group_menu_permissions | 그룹_메뉴_권한 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 메뉴그룹권한ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| group_id | 그룹ID | bigint | - | - | Y | (복합유니크: group_id, menu_id, is_deleted) | N | groups.id |
|
||||||
|
| menu_id | 메뉴ID | bigint | - | - | Y | (복합유니크: group_id, menu_id, is_deleted) | N | menus.id |
|
||||||
|
| can_create | 생성권한 | boolean | - | false | Y | | | - |
|
||||||
|
| can_read | 조회권한 | boolean | - | true | Y | | | - |
|
||||||
|
| can_update | 수정권한 | boolean | - | false | Y | | | - |
|
||||||
|
| can_delete | 삭제권한 | boolean | - | false | Y | | | - |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 각 메뉴에 대한 CRUD 권한을 그룹 단위로 정의하며, 권한 미설정 시 기본적으로 조회만 허용.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.10 `uoms` (단위) — 타입 테이블
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| uoms | 단위 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 단위ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| uom_name | 단위명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
|
||||||
|
| is_default | 기본여부 | boolean | - | false | Y | | | |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 예시 값: `EA`(기본 단위), `BOX`, `KG`, `LITER` 등.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.11 `products` (제품)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| products | 제품 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 제품ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| product_code | 제품코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| product_name | 제품명 | varchar | 100 | - | Y | | | |
|
||||||
|
| vendor_id | 벤더ID | bigint | - | - | Y | | | vendors.id |
|
||||||
|
| uom_id | 단위ID | bigint | - | - | Y | | | uoms.id |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.12 `transaction_types` (입출고_유형) — 타입 테이블
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| transaction_types | 입출고_유형 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 유형ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| type_name | 유형명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
|
||||||
|
| is_default | 기본여부 | boolean | - | false | Y | | | |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 예시 값: `입고`(is_default=true), `출고`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.13 `transaction_statuses` (트랜잭션_상태) — 타입 테이블
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| transaction_statuses | 트랜잭션_상태 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 상태ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| status_name | 상태명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
|
||||||
|
| is_default | 기본여부 | boolean | - | false | Y | | | |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 예시 값: `정상`(is_default), `반품`, `폐기`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.14 `stock_transactions` (입출고_트랜잭션)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| stock_transactions | 입출고_트랜잭션 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 트랜잭션ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| transaction_no | 트랜잭션번호 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| transaction_type_id | 입출고유형ID | bigint | - | - | Y | | | transaction_types.id |
|
||||||
|
| transaction_status_id | 트랜잭션상태ID | bigint | - | - | Y | | | transaction_statuses.id |
|
||||||
|
| warehouse_id | 창고ID | bigint | - | - | Y | | | warehouses.id |
|
||||||
|
| transaction_date | 처리일자 | date | - | current_date | Y | | | - |
|
||||||
|
| created_by_id | 작성자ID | bigint | - | - | N | | | employees.id |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 주의: **벤더ID 없음**. 벤더 정보는 라인의 `product_id`가 가리키는 `products.vendor_id`로 파생.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.15 `transaction_lines` (트랜잭션_라인)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| transaction_lines | 트랜잭션_라인 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 라인ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| transaction_id | 트랜잭션ID | bigint | - | - | Y | | | stock_transactions.id |
|
||||||
|
| line_no | 라인번호 | integer | - | 1 | Y | (복합유니크: transaction_id, line_no, is_deleted) | N | - |
|
||||||
|
| product_id | 제품ID | bigint | - | - | Y | | | products.id |
|
||||||
|
| quantity | 수량 | numeric | 20,6 | 0 | Y | | | - |
|
||||||
|
| unit_price | 단가 | numeric | 20,6 | 0 | N | | | - |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.16 `transaction_customers` (트랜잭션_고객사)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| transaction_customers | 트랜잭션_고객사 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 키 | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| transaction_id | 트랜잭션ID | bigint | - | - | Y | | | stock_transactions.id |
|
||||||
|
| customer_id | 고객사ID | bigint | - | - | Y | (복합유니크: transaction_id, customer_id, is_deleted) | N | customers.id |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.17 `approval_statuses` (결재_상태) — 타입 테이블
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| approval_statuses | 결재_상태 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 상태ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| status_name | 상태명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
|
||||||
|
| is_default | 기본여부 | boolean | - | false | Y | | | |
|
||||||
|
| is_blocking_next | 차기이동차단 | boolean | - | true | Y | | | |
|
||||||
|
| is_terminal | 종결여부 | boolean | - | false | Y | | | |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 예시 값:
|
||||||
|
> - `대기`(`is_default=true`, `is_blocking_next=true`, `is_terminal=false`)
|
||||||
|
> - `진행중`(`is_blocking_next=true`, `is_terminal=false`)
|
||||||
|
> - `보류`(`is_blocking_next=true`, `is_terminal=false`)
|
||||||
|
> - `승인`(`is_blocking_next=false`, `is_terminal=false`)
|
||||||
|
> - `반려`(`is_blocking_next=true`, `is_terminal=true`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.18 `approval_actions` (결재_행위) — 타입 테이블
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| approval_actions | 결재_행위 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 행위ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| action_name | 행위명 | varchar | 100 | - | Y | (선택) 부분유니크 | N | - |
|
||||||
|
| is_default | 기본여부 | boolean | - | false | Y | | | |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 예시 값: `approve`(승인), `reject`(반려), `comment`(코멘트).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.19 `approvals` (결재)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| approvals | 결재 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 결재ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| transaction_id | 트랜잭션ID | bigint | - | - | Y | (부분유니크: is_deleted=false) | N | stock_transactions.id |
|
||||||
|
| approval_no | 결재번호 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| approval_status_id | 전체결재상태ID | bigint | - | - | Y | | | approval_statuses.id |
|
||||||
|
| current_step_id | 현재단계ID | bigint | - | - | N | | | approval_steps.id |
|
||||||
|
| requested_by_id | 상신자ID | bigint | - | - | Y | | | employees.id |
|
||||||
|
| requested_at | 상신일시 | timestamp | - | now() | Y | | | - |
|
||||||
|
| decided_at | 최종결정일시 | timestamp | - | - | N | | | - |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.20 `approval_steps` (결재_단계)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| approval_steps | 결재_단계 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 단계ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| approval_id | 결재ID | bigint | - | - | Y | | | approvals.id |
|
||||||
|
| step_order | 단계순서 | integer | - | 1 | Y | (복합유니크: approval_id, step_order, is_deleted) | N | - |
|
||||||
|
| approver_id | 승인자ID | bigint | - | - | Y | | | employees.id |
|
||||||
|
| step_status_id | 단계상태ID | bigint | - | - | Y | | | approval_statuses.id |
|
||||||
|
| assigned_at | 배정일시 | timestamp | - | now() | Y | | | - |
|
||||||
|
| decided_at | 결정일시 | timestamp | - | - | N | | | - |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.21 `approval_histories` (결재_승인이력)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| approval_histories | 결재_승인이력 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 이력ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| approval_id | 결재ID | bigint | - | - | Y | | | approvals.id |
|
||||||
|
| approval_step_id | 결재단계ID | bigint | - | - | Y | | | approval_steps.id |
|
||||||
|
| approver_id | 승인자ID | bigint | - | - | Y | | | employees.id |
|
||||||
|
| approval_action_id | 결재행위ID | bigint | - | - | Y | | | approval_actions.id |
|
||||||
|
| from_status_id | 변경전상태ID | bigint | - | - | N | | | approval_statuses.id |
|
||||||
|
| to_status_id | 변경후상태ID | bigint | - | - | Y | | | approval_statuses.id |
|
||||||
|
| action_at | 작업일시 | timestamp | - | now() | Y | | | - |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.22 `approval_templates` (결재_템플릿)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| approval_templates | 결재_템플릿 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 템플릿ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| template_code | 템플릿코드 | varchar | 30 | - | Y | (부분유니크: is_deleted=false) | N | - |
|
||||||
|
| template_name | 템플릿명 | varchar | 100 | - | Y | | | |
|
||||||
|
| description | 설명 | varchar | 255 | - | N | | | - |
|
||||||
|
| created_by_id | 작성자ID | bigint | - | - | Y | | | employees.id |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.23 `approval_template_steps` (결재_템플릿_단계)
|
||||||
|
| 영문테이블명 | 한글테이블명 |
|
||||||
|
|---|---|
|
||||||
|
| approval_template_steps | 결재_템플릿_단계 |
|
||||||
|
|
||||||
|
| 영문필드명 | 한글필드명 | 타입 | 길이 | 기본값 | NOT NULL | UNIQUE | PK | FK |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| id | 템플릿단계ID | bigint | - | identity | Y | Y | Y | - |
|
||||||
|
| template_id | 템플릿ID | bigint | - | - | Y | (복합유니크: template_id, step_order, is_deleted) | N | approval_templates.id |
|
||||||
|
| step_order | 단계순서 | integer | - | 1 | Y | (복합유니크: template_id, step_order, is_deleted) | N | - |
|
||||||
|
| approver_id | 승인자ID | bigint | - | - | Y | | | employees.id |
|
||||||
|
| note | 비고 | text | - | - | N | | | - |
|
||||||
|
| is_active | 사용여부 | boolean | - | true | Y | | | |
|
||||||
|
| is_deleted | 삭제여부 | boolean | - | false | Y | | | |
|
||||||
|
| created_at | 생성일시 | timestamp | - | now() | Y | | | |
|
||||||
|
| updated_at | 변경일시 | timestamp | - | now() | Y | | | |
|
||||||
|
|
||||||
|
> 템플릿 단계는 실제 결재 단계 생성 시 그대로 복제되며, `step_order` 순서대로 승인자가 배치됨.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) FK 관계 (source → target)
|
||||||
|
- `menus.parent_menu_id` → `menus.id`
|
||||||
|
- `employees.group_id` → `groups.id`
|
||||||
|
- `group_menu_permissions.group_id` → `groups.id`
|
||||||
|
- `group_menu_permissions.menu_id` → `menus.id`
|
||||||
|
- `warehouses.zipcode` → `zipcodes.zipcode`
|
||||||
|
- `customers.zipcode` → `zipcodes.zipcode`
|
||||||
|
- `products.vendor_id` → `vendors.id`
|
||||||
|
- `products.uom_id` → `uoms.id`
|
||||||
|
- `stock_transactions.warehouse_id` → `warehouses.id`
|
||||||
|
- `stock_transactions.created_by_id` → `employees.id`
|
||||||
|
- `stock_transactions.transaction_type_id` → `transaction_types.id`
|
||||||
|
- `stock_transactions.transaction_status_id` → `transaction_statuses.id`
|
||||||
|
- `transaction_lines.transaction_id` → `stock_transactions.id`
|
||||||
|
- `transaction_lines.product_id` → `products.id`
|
||||||
|
- `transaction_customers.transaction_id` → `stock_transactions.id`
|
||||||
|
- `transaction_customers.customer_id` → `customers.id`
|
||||||
|
- `approvals.transaction_id` → `stock_transactions.id`
|
||||||
|
- `approvals.approval_status_id` → `approval_statuses.id`
|
||||||
|
- `approvals.current_step_id` → `approval_steps.id`
|
||||||
|
- `approvals.requested_by_id` → `employees.id`
|
||||||
|
- `approval_steps.approval_id` → `approvals.id`
|
||||||
|
- `approval_steps.approver_id` → `employees.id`
|
||||||
|
- `approval_steps.step_status_id` → `approval_statuses.id`
|
||||||
|
- `approval_histories.approval_id` → `approvals.id`
|
||||||
|
- `approval_histories.approval_step_id` → `approval_steps.id`
|
||||||
|
- `approval_histories.approver_id` → `employees.id`
|
||||||
|
- `approval_histories.approval_action_id` → `approval_actions.id`
|
||||||
|
- `approval_histories.from_status_id` → `approval_statuses.id`
|
||||||
|
- `approval_histories.to_status_id` → `approval_statuses.id`
|
||||||
|
- `approval_templates.created_by_id` → `employees.id`
|
||||||
|
- `approval_template_steps.template_id` → `approval_templates.id`
|
||||||
|
- `approval_template_steps.approver_id` → `employees.id`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) 비즈니스/검증 규칙
|
||||||
|
- 제품 등록 시 `vendor_id` **필수**.
|
||||||
|
- 입고(`transaction_type_id`=입고) 트랜잭션의 공급자 정보는 **라인 제품의 벤더**로만 해석. (헤더에 벤더 금지)
|
||||||
|
- 출고 트랜잭션은 `transaction_customers` **최소 1건** 필요.
|
||||||
|
- 결재는 **트랜잭션당 1건**(미삭제 기준)만 허용.
|
||||||
|
- 단계 전이는 **현재 단계**에서만 수행 가능. blocking 상태에서는 차기 이동 불가.
|
||||||
|
- 수량/단가 음수 금지(CHECK).
|
||||||
|
- 그룹이 비활성(`is_active=false`) 또는 삭제되면 해당 그룹 권한/구성원은 즉시 무효 처리.
|
||||||
|
- 사원의 소속 그룹(`employees.group_id`)에서 해당 메뉴에 대한 `can_create|can_update|can_delete` 중 하나라도 true이면 그 동작을 수행할 수 있음.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) 인덱스/유니크 권장
|
||||||
|
- 부분 유니크(또는 복합 유니크)로 소프트 삭제와 공존:
|
||||||
|
- `vendors(vendor_code)`, `warehouses(warehouse_code)`, `customers(customer_code)`, `employees(employee_no)`, `menus(menu_code)`, `groups(group_name)`, `zipcodes(zipcode)`, `products(product_code)`, `stock_transactions(transaction_no)`, `approvals(approval_no)`
|
||||||
|
- `group_menu_permissions(group_id, menu_id, is_deleted)`
|
||||||
|
- `approvals(transaction_id)` — 미삭제 조건에서 1:1 보장
|
||||||
|
- `transaction_lines(transaction_id, line_no, is_deleted)`
|
||||||
|
- `transaction_customers(transaction_id, customer_id, is_deleted)`
|
||||||
|
- FK 및 조회 인덱스: 모든 `*_id`, `updated_at`, `is_deleted`, `is_active`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) 에러 규격(예시)
|
||||||
|
- `400 BAD_REQUEST` — 필수 필드 누락, 형식 오류
|
||||||
|
- `409 CONFLICT` — 유니크 충돌(코드/번호/조합), **현재 단계 아님**
|
||||||
|
- `422 UNPROCESSABLE_ENTITY` — 비즈니스 규칙 위반(출고인데 고객 없음, blocking 상태에서 이동 등)
|
||||||
|
- `404 NOT_FOUND` — 리소스 없음 또는 삭제됨(`deleted=false` 기본 필터로 미노출)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) 마이그레이션 가이드(요약)
|
||||||
|
1) `stock_transactions`에서 `vendor_id` 드롭.
|
||||||
|
2) `customer_roles` 테이블 및 관련 컬럼 드롭.
|
||||||
|
3) 모든 타입/코드 테이블에 공통 컬럼 4종 추가(미존재 시).
|
||||||
|
4) 부분 유니크 인덱스(`WHERE is_deleted=false`) 또는 `(컬럼, is_deleted)` 복합 유니크 구성.
|
||||||
|
5) 기존 결재 이력은 `approval_step_id` 매핑(없으면 1단계로 귀속).
|
||||||
|
6) `approval_statuses`에 `is_blocking_next`, `is_terminal` 값 시드.
|
||||||
|
7) `menus`, `groups`, `group_menu_permissions` 신규 생성 및 기존 관리자 권한/사원-그룹 매핑을 `employees.group_id`로 이관.
|
||||||
|
8) `zipcodes` 테이블 생성 및 도로명 주소 기준 데이터 적재.
|
||||||
|
9) 모든 테이블에 `note`(text) 컬럼 추가 및 필요한 경우 기본값 NULL 유지.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9) 초기 시드 값(예시)
|
||||||
|
- `transaction_types`: [입고, 출고] (`is_default`: 입고)
|
||||||
|
- `transaction_statuses`: [초안, 상신, 승인, 반려, 완료] (`is_default`: 초안)
|
||||||
|
- `approval_statuses`: [대기(pending, default, blocking), 진행중(in_progress, blocking), 보류(on_hold, blocking), 승인(approved, !blocking), 반려(rejected, blocking+terminal)]
|
||||||
|
- `approval_actions`: [승인(approve), 반려(reject), 코멘트(comment)]
|
||||||
|
- `uoms`: [EA(기본), BOX, KG ...]
|
||||||
|
- `menus`: [대시보드, 입출고 관리, 결재 관리, 레포트 등] — 상위/하위 메뉴 구조 포함
|
||||||
|
- `groups`: [전사 관리자(기본), 창고 관리자, 결재 담당자]
|
||||||
|
- `group_menu_permissions`: 기본 그룹별 메뉴 권한(CRUD 플래그); 전사 관리자는 모든 메뉴 `can_*`=true, 역할별로 세분화 설정
|
||||||
|
- `zipcodes`: 행정안전부 도로명 주소 DB(5자리) 최신본을 기준으로 일괄 적재
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10) 구현 팁
|
||||||
|
- `updated_at` 자동 갱신 트리거, 소프트 삭제 처리 트리거 권장.
|
||||||
|
- 낙관적 잠금(선택): `version`(int) + ETag.
|
||||||
|
- 병렬 결재 확장(선택): `approval_steps`에 `group_no`, `approval_mode(all|any)` 도입.
|
||||||
225
doc/입출고 대여 폼 정리.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
flutter 사용
|
||||||
|
[flutter shadCnUI](https://github.com/nank1ro/flutter-shadcn-ui) 라이브러리 사용.
|
||||||
|
반응형웹서비스.
|
||||||
|
API연결없이 화면 구성만. 단 네비게이션은 작동되어야 함.
|
||||||
|
프론트엔드 화면만 구현.
|
||||||
|
git 저장소 없음
|
||||||
|
|
||||||
|
# 입·출고 + 결재 시스템 전체 UI 분석 & 와이어프레임 (v2)
|
||||||
|
|
||||||
|
## 0. 로그인/로그아웃
|
||||||
|
### 입력 폼
|
||||||
|
- 아이디(사번 또는 이메일) [TXT]
|
||||||
|
- 비밀번호 [PWD]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 없음 (세션 기반)
|
||||||
|
|
||||||
|
### 테이블 리스트
|
||||||
|
- 없음
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 대시보드
|
||||||
|
### 주요 위젯
|
||||||
|
- 오늘 입고/출고 건수, 대기 결재 수
|
||||||
|
- 최근 트랜잭션 리스트 (번호, 일자, 유형, 상태, 작성자)
|
||||||
|
- 내 결재 요청/대기 건
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 입고
|
||||||
|
### 입력 폼
|
||||||
|
- 처리일자[DT], 창고[DD], 상태[DD], 작성자[RO], 비고[TXT]
|
||||||
|
- 라인: 제품[DD], 제조사[RO], 단위[RO], 수량[NUM], 단가[NUM], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 작성자[RO], 트랜잭션번호[RO], 상태[일부 제한]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 처리일자 | 창고 | 트랜잭션번호 | 상태 | 작성자 | 품목수 | 총수량 | 비고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 출고
|
||||||
|
### 입력 폼
|
||||||
|
- 처리일자[DT], 창고[DD], 상태[DD], 작성자[RO], 비고[TXT]
|
||||||
|
- 라인: 제품[DD], 제조사[RO], 단위[RO], 수량[NUM], 단가[NUM], 비고[TXT]
|
||||||
|
- 고객사 연결: 고객사[DD-멀티], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 작성자[RO], 트랜잭션번호[RO], 고객사[수정 가능], 상태[제한]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 처리일자 | 창고 | 트랜잭션번호 | 상태 | 작성자 | 고객수 | 품목수 | 총수량 | 비고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 대여
|
||||||
|
### 입력 폼
|
||||||
|
- 처리일자[DT], 창고[DD], 상태[DD], 작성자[RO], 대여구분[DD], 반납예정일[DT], 비고[TXT]
|
||||||
|
- 라인: 제품[DD], 제조사[RO], 단위[RO], 수량[NUM], 단가[NUM], 비고[TXT]
|
||||||
|
- 고객사 연결: 고객사[DD-멀티], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 작성자[RO], 트랜잭션번호[RO], 대여구분[제한], 반납예정일[수정가능]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 처리일자 | 창고 | 대여/반납 | 트랜잭션번호 | 상태 | 반납예정일 | 고객수 | 품목수 | 비고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 제조사 관리 (벤더)
|
||||||
|
### 입력 폼
|
||||||
|
- 벤더코드[TXT], 벤더명[TXT], 사용여부[SW], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 벤더코드[RO], 생성일시[RO], 수정일시[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 벤더코드 | 벤더명 | 사용여부 | 비고 | 변경일시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 장비 모델 관리 (제품)
|
||||||
|
### 입력 폼
|
||||||
|
- 제품코드[TXT], 제품명[TXT], 제조사[DD], 단위[DD], 사용여부[SW], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 제품코드[RO], 생성일시[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 제품코드 | 제품명 | 제조사 | 단위 | 사용여부 | 비고 | 변경일시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 입고지 관리 (창고)
|
||||||
|
### 입력 폼
|
||||||
|
- 창고코드[TXT], 창고명[TXT], 우편번호[검색], 상세주소[TXT], 사용여부[SW], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 창고코드[RO], 생성일시[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 창고코드 | 창고명 | 우편번호 | 상세주소 | 사용여부 | 비고 | 변경일시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 회사 관리 (고객사)
|
||||||
|
### 입력 폼
|
||||||
|
- 고객사코드[TXT], 고객사명[TXT], 유형(파트너/일반)[DD], 이메일[TXT], 연락처[TXT], 우편번호[검색], 상세주소[TXT], 사용여부[SW], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 고객사코드[RO], 생성일시[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 고객사코드 | 고객사명 | 유형 | 이메일 | 연락처 | 우편번호 | 상세주소 | 사용여부 | 비고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 사용자 관리 (사원)
|
||||||
|
### 입력 폼
|
||||||
|
- 사번[TXT], 성명[TXT], 이메일[TXT], 연락처[TXT], 그룹[DD], 사용여부[SW], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 사번[RO], 생성일시[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 사번 | 성명 | 이메일 | 연락처 | 그룹 | 사용여부 | 비고 | 변경일시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 그룹 관리
|
||||||
|
### 입력 폼
|
||||||
|
- 그룹명[TXT], 그룹설명[TXT], 기본여부[SW], 사용여부[SW], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 그룹명[RO], 생성일시[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 그룹명 | 설명 | 기본여부 | 사용여부 | 비고 | 변경일시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 메뉴 관리
|
||||||
|
### 입력 폼
|
||||||
|
- 메뉴코드[TXT], 메뉴명[TXT], 상위메뉴[DD], 경로[TXT], 표시순서[NUM], 사용여부[SW], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 메뉴코드[RO], 생성일시[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 메뉴코드 | 메뉴명 | 상위메뉴 | 경로 | 사용여부 | 비고 | 변경일시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 그룹 메뉴 권한 관리
|
||||||
|
### 입력 폼
|
||||||
|
- 그룹[DD], 메뉴[DD], 생성권한[CHK], 조회권한[CHK], 수정권한[CHK], 삭제권한[CHK], 사용여부[SW]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 그룹[RO], 메뉴[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 그룹명 | 메뉴명 | 생성 | 조회 | 수정 | 삭제 | 사용여부 | 변경일시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 결재 관리
|
||||||
|
### 입력 폼
|
||||||
|
- 트랜잭션번호[DD], 결재번호[자동생성], 결재상태[DD], 상신자[자동], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 결재번호[RO], 상신자[RO], 요청일시[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 결재번호 | 트랜잭션번호 | 상태 | 상신자 | 요청일시 | 최종결정일시 | 비고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. 결재 단계 관리
|
||||||
|
### 입력 폼
|
||||||
|
- 결재ID[DD], 단계순서[NUM], 승인자[DD], 단계상태[DD], 비고[TXT]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 결재ID[RO], 단계순서[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 결재ID | 단계순서 | 승인자 | 상태 | 배정일시 | 결정일시 | 비고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. 결재 이력 조회
|
||||||
|
- 테이블 전용 (수정 없음)
|
||||||
|
- 컬럼: 번호 | 결재ID | 단계ID | 승인자 | 행위 | 변경전상태 | 변경후상태 | 작업일시 | 비고
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. 결재 템플릿 관리
|
||||||
|
### 입력 폼
|
||||||
|
- 템플릿코드[TXT], 템플릿명[TXT], 설명[TXT], 작성자[RO], 사용여부[SW], 비고[TXT]
|
||||||
|
- 단계: (+추가) 순서[NUM], 승인자[DD]
|
||||||
|
|
||||||
|
### 수정 폼
|
||||||
|
- 템플릿코드[RO], 작성자[RO]
|
||||||
|
|
||||||
|
### 테이블 리스트 (1행)
|
||||||
|
번호 | 템플릿코드 | 템플릿명 | 설명 | 작성자 | 사용여부 | 변경일시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. 우편번호 관리 (검색용)
|
||||||
|
- 모달 전용: 검색어[TXT], 결과 리스트 (우편번호 | 시도 | 시군구 | 도로명 | 건물번호)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. 보고서
|
||||||
|
### 화면
|
||||||
|
- 조건 입력: 기간[DT-기간], 유형[DD], 창고[DD], 상태[DD]
|
||||||
|
- 출력: [BTN: XLSX 다운로드], [BTN: PDF 다운로드]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ✅ 최종 요약
|
||||||
|
- 로그인 → 대시보드 → 입고/출고/대여 → 마스터 관리(벤더, 제품, 창고, 고객사, 사용자, 그룹, 메뉴, 권한) → 결재 관리(결재, 단계, 이력, 템플릿) → 보고서 → 로그아웃까지 전부 포함.
|
||||||
|
- 각 페이지마다 입력폼, 수정폼, 테이블리스트 1행 예시를 일관되게 정리함.
|
||||||
34
ios/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
26
ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>13.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
2
ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
2
ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
43
ios/Podfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Uncomment this line to define a global platform for your project
|
||||||
|
# platform :ios, '13.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
616
ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.superportV2;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.superportV2.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.superportV2.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.superportV2.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
97C147061CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.superportV2;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147071CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.superportV2;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C8088294A63A400263BE5 /* Debug */,
|
||||||
|
331C8089294A63A400263BE5 /* Release */,
|
||||||
|
331C808A294A63A400263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
101
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
13
ios/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
26
ios/Runner/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
49
ios/Runner/Info.plist
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Superport V2</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>superport_v2</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
ios/Runner/Runner-Bridging-Header.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
12
ios/RunnerTests/RunnerTests.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
65
lib/core/config/environment.dart
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
||||||
|
/// 환경 설정 로더
|
||||||
|
///
|
||||||
|
/// - .env.development / .env.production 파일을 로드하여 런타임 설정을 주입한다.
|
||||||
|
/// - `--dart-define=ENV=production` 형태로 빌드/실행 시 환경을 지정한다.
|
||||||
|
/// - 주요 키: `API_BASE_URL`, `FEATURE_*` 플래그들.
|
||||||
|
class Environment {
|
||||||
|
Environment._();
|
||||||
|
|
||||||
|
/// 현재 환경명 (development | production)
|
||||||
|
static late final String envName;
|
||||||
|
|
||||||
|
/// API 서버 베이스 URL
|
||||||
|
static late final String baseUrl;
|
||||||
|
|
||||||
|
/// 프로덕션 여부
|
||||||
|
static late final bool isProduction;
|
||||||
|
|
||||||
|
/// 환경 초기화
|
||||||
|
///
|
||||||
|
/// - 기본 환경은 development이며, `ENV` dart-define 으로 변경 가능
|
||||||
|
/// - 해당 환경의 .env 파일을 로드하고 핵심 값을 추출한다.
|
||||||
|
static Future<void> initialize() async {
|
||||||
|
const envFromDefine = String.fromEnvironment('ENV', defaultValue: 'development');
|
||||||
|
envName = envFromDefine.toLowerCase();
|
||||||
|
isProduction = envName == 'production';
|
||||||
|
|
||||||
|
final fileName = '.env.$envName';
|
||||||
|
try {
|
||||||
|
await dotenv.load(fileName: fileName);
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
// 개발 편의를 위해 파일 미존재 시 경고만 출력하고 진행
|
||||||
|
// 실제 배포에서는 파일 존재가 필수다.
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[Environment] $fileName 로드 실패: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseUrl = dotenv.maybeGet('API_BASE_URL') ?? 'http://localhost:8080';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 기능 플래그 조회 (기본 false)
|
||||||
|
static bool flag(String key, {bool defaultValue = false}) {
|
||||||
|
final v = dotenv.maybeGet(key);
|
||||||
|
if (v == null) return defaultValue;
|
||||||
|
switch (v.trim().toLowerCase()) {
|
||||||
|
case '1':
|
||||||
|
case 'y':
|
||||||
|
case 'yes':
|
||||||
|
case 'true':
|
||||||
|
return true;
|
||||||
|
case '0':
|
||||||
|
case 'n':
|
||||||
|
case 'no':
|
||||||
|
case 'false':
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
171
lib/core/constants/app_sections.dart
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
|
||||||
|
class AppPageDescriptor {
|
||||||
|
const AppPageDescriptor({
|
||||||
|
required this.path,
|
||||||
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
required this.summary,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
final String summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppSectionDescriptor {
|
||||||
|
const AppSectionDescriptor({required this.label, required this.pages});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final List<AppPageDescriptor> pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginRoutePath = '/login';
|
||||||
|
const dashboardRoutePath = '/dashboard';
|
||||||
|
|
||||||
|
const appSections = <AppSectionDescriptor>[
|
||||||
|
AppSectionDescriptor(
|
||||||
|
label: '대시보드',
|
||||||
|
pages: [
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: dashboardRoutePath,
|
||||||
|
label: '대시보드',
|
||||||
|
icon: LucideIcons.layoutDashboard,
|
||||||
|
summary: '오늘 입고/출고, 결재 대기, 최근 트랜잭션을 한 화면에서 확인합니다.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AppSectionDescriptor(
|
||||||
|
label: '입·출고',
|
||||||
|
pages: [
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/inventory/inbound',
|
||||||
|
label: '입고',
|
||||||
|
icon: LucideIcons.packagePlus,
|
||||||
|
summary: '입고 처리 기본정보와 라인 품목을 등록하고 검토합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/inventory/outbound',
|
||||||
|
label: '출고',
|
||||||
|
icon: LucideIcons.packageMinus,
|
||||||
|
summary: '출고 품목, 고객사 연결, 상태 변경을 관리합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/inventory/rental',
|
||||||
|
label: '대여',
|
||||||
|
icon: LucideIcons.handshake,
|
||||||
|
summary: '대여/반납 구분과 반납예정일을 포함한 대여 흐름입니다.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AppSectionDescriptor(
|
||||||
|
label: '마스터',
|
||||||
|
pages: [
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/masters/vendors',
|
||||||
|
label: '제조사 관리',
|
||||||
|
icon: LucideIcons.factory,
|
||||||
|
summary: '벤더코드, 명칭, 사용여부 등을 유지합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/masters/products',
|
||||||
|
label: '장비 모델 관리',
|
||||||
|
icon: LucideIcons.box,
|
||||||
|
summary: '제품코드, 제조사, 단위 정보를 관리합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/masters/warehouses',
|
||||||
|
label: '입고지 관리',
|
||||||
|
icon: LucideIcons.warehouse,
|
||||||
|
summary: '창고 주소와 사용여부를 설정합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/masters/customers',
|
||||||
|
label: '회사 관리',
|
||||||
|
icon: LucideIcons.building,
|
||||||
|
summary: '고객사 연락처와 주소 정보를 관리합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/masters/users',
|
||||||
|
label: '사용자 관리',
|
||||||
|
icon: LucideIcons.users,
|
||||||
|
summary: '사번, 그룹, 사용여부를 관리합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/masters/groups',
|
||||||
|
label: '그룹 관리',
|
||||||
|
icon: LucideIcons.layers,
|
||||||
|
summary: '권한 그룹과 설명, 기본여부를 정의합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/masters/menus',
|
||||||
|
label: '메뉴 관리',
|
||||||
|
icon: LucideIcons.listTree,
|
||||||
|
summary: '메뉴 계층과 경로, 노출 순서를 구성합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/masters/group-permissions',
|
||||||
|
label: '그룹 메뉴 권한',
|
||||||
|
icon: LucideIcons.shieldCheck,
|
||||||
|
summary: '그룹별 메뉴 CRUD 권한을 설정합니다.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AppSectionDescriptor(
|
||||||
|
label: '결재',
|
||||||
|
pages: [
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/approvals/requests',
|
||||||
|
label: '결재 관리',
|
||||||
|
icon: LucideIcons.fileCheck,
|
||||||
|
summary: '결재 번호, 상태, 상신자를 관리합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/approvals/steps',
|
||||||
|
label: '결재 단계',
|
||||||
|
icon: LucideIcons.workflow,
|
||||||
|
summary: '단계 순서와 승인자 할당을 설정합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/approvals/history',
|
||||||
|
label: '결재 이력',
|
||||||
|
icon: LucideIcons.history,
|
||||||
|
summary: '결재 단계별 변경 이력을 조회합니다.',
|
||||||
|
),
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/approvals/templates',
|
||||||
|
label: '결재 템플릿',
|
||||||
|
icon: LucideIcons.fileSpreadsheet,
|
||||||
|
summary: '반복되는 결재 흐름을 템플릿으로 관리합니다.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AppSectionDescriptor(
|
||||||
|
label: '도구',
|
||||||
|
pages: [
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/utilities/postal-search',
|
||||||
|
label: '우편번호 검색',
|
||||||
|
icon: LucideIcons.search,
|
||||||
|
summary: '모달 기반 우편번호 검색 도구입니다.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AppSectionDescriptor(
|
||||||
|
label: '보고',
|
||||||
|
pages: [
|
||||||
|
AppPageDescriptor(
|
||||||
|
path: '/reports',
|
||||||
|
label: '보고서',
|
||||||
|
icon: LucideIcons.fileDown,
|
||||||
|
summary: '조건 필터와 PDF/XLSX 다운로드 기능입니다.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
List<AppPageDescriptor> get allAppPages => [
|
||||||
|
for (final section in appSections) ...section.pages,
|
||||||
|
];
|
||||||
60
lib/core/network/api_client.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
/// 공통 API 클라이언트 (Dio 래퍼)
|
||||||
|
/// - 모든 HTTP 호출은 이 클래스를 통해 이루어진다.
|
||||||
|
/// - BaseURL/타임아웃/인증/로깅/에러 처리 등을 중앙집중화한다.
|
||||||
|
class ApiClient {
|
||||||
|
final Dio _dio;
|
||||||
|
|
||||||
|
/// 내부에서 사용하는 Dio 인스턴스
|
||||||
|
/// 외부에서 Dio 직접 사용을 최소화하고, 가능하면 아래 헬퍼 메서드를 사용한다.
|
||||||
|
Dio get dio => _dio;
|
||||||
|
|
||||||
|
ApiClient({required Dio dio}) : _dio = dio;
|
||||||
|
|
||||||
|
/// GET 요청 헬퍼
|
||||||
|
Future<Response<T>> get<T>(
|
||||||
|
String path, {
|
||||||
|
Map<String, dynamic>? query,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
}) {
|
||||||
|
return _dio.get<T>(path, queryParameters: query, options: options, cancelToken: cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST 요청 헬퍼
|
||||||
|
Future<Response<T>> post<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? query,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
}) {
|
||||||
|
return _dio.post<T>(path, data: data, queryParameters: query, options: options, cancelToken: cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PATCH 요청 헬퍼
|
||||||
|
Future<Response<T>> patch<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? query,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
}) {
|
||||||
|
return _dio.patch<T>(path, data: data, queryParameters: query, options: options, cancelToken: cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DELETE 요청 헬퍼
|
||||||
|
Future<Response<T>> delete<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? query,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
}) {
|
||||||
|
return _dio.delete<T>(path, data: data, queryParameters: query, options: options, cancelToken: cancelToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
29
lib/core/network/interceptors/auth_interceptor.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
/// 인증 인터셉터(스켈레톤)
|
||||||
|
/// - 요청 전에 Authorization 헤더 주입
|
||||||
|
/// - 401 수신 시 토큰 갱신 및 원요청 1회 재시도 (구현 예정)
|
||||||
|
class AuthInterceptor extends Interceptor {
|
||||||
|
/// TODO: 토큰 저장/조회 서비스 주입 (예: AuthRepository)
|
||||||
|
AuthInterceptor();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||||
|
// TODO: 저장된 토큰을 읽어 Authorization 헤더에 주입한다.
|
||||||
|
// final token = await _authRepository.getToken();
|
||||||
|
// if (token != null && token.isNotEmpty) {
|
||||||
|
// options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
// }
|
||||||
|
handler.next(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||||
|
// TODO: 401 처리 로직(토큰 갱신 → 원요청 재시도) 구현
|
||||||
|
// if (err.response?.statusCode == 401) { ... }
|
||||||
|
handler.next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
135
lib/core/routing/app_router.dart
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../../features/approvals/history/presentation/pages/approval_history_page.dart';
|
||||||
|
import '../../features/approvals/request/presentation/pages/approval_request_page.dart';
|
||||||
|
import '../../features/approvals/step/presentation/pages/approval_step_page.dart';
|
||||||
|
import '../../features/approvals/template/presentation/pages/approval_template_page.dart';
|
||||||
|
import '../../features/dashboard/presentation/pages/dashboard_page.dart';
|
||||||
|
import '../../features/inventory/inbound/presentation/pages/inbound_page.dart';
|
||||||
|
import '../../features/inventory/outbound/presentation/pages/outbound_page.dart';
|
||||||
|
import '../../features/inventory/rental/presentation/pages/rental_page.dart';
|
||||||
|
import '../../features/login/presentation/pages/login_page.dart';
|
||||||
|
import '../../features/masters/customer/presentation/pages/customer_page.dart';
|
||||||
|
import '../../features/masters/group/presentation/pages/group_page.dart';
|
||||||
|
import '../../features/masters/group_permission/presentation/pages/group_permission_page.dart';
|
||||||
|
import '../../features/masters/menu/presentation/pages/menu_page.dart';
|
||||||
|
import '../../features/masters/product/presentation/pages/product_page.dart';
|
||||||
|
import '../../features/masters/user/presentation/pages/user_page.dart';
|
||||||
|
import '../../features/masters/vendor/presentation/pages/vendor_page.dart';
|
||||||
|
import '../../features/masters/warehouse/presentation/pages/warehouse_page.dart';
|
||||||
|
import '../../features/reporting/presentation/pages/reporting_page.dart';
|
||||||
|
import '../../features/util/postal_search/presentation/pages/postal_search_page.dart';
|
||||||
|
import '../../widgets/app_shell.dart';
|
||||||
|
import '../constants/app_sections.dart';
|
||||||
|
|
||||||
|
final _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||||
|
|
||||||
|
final appRouter = GoRouter(
|
||||||
|
navigatorKey: _rootNavigatorKey,
|
||||||
|
initialLocation: loginRoutePath,
|
||||||
|
routes: [
|
||||||
|
GoRoute(path: '/', redirect: (_, __) => loginRoutePath),
|
||||||
|
GoRoute(
|
||||||
|
path: loginRoutePath,
|
||||||
|
name: 'login',
|
||||||
|
builder: (context, state) => const LoginPage(),
|
||||||
|
),
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) =>
|
||||||
|
AppShell(currentLocation: state.uri.toString(), child: child),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: dashboardRoutePath,
|
||||||
|
name: 'dashboard',
|
||||||
|
builder: (context, state) => const DashboardPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/inventory/inbound',
|
||||||
|
name: 'inventory-inbound',
|
||||||
|
builder: (context, state) => const InboundPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/inventory/outbound',
|
||||||
|
name: 'inventory-outbound',
|
||||||
|
builder: (context, state) => const OutboundPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/inventory/rental',
|
||||||
|
name: 'inventory-rental',
|
||||||
|
builder: (context, state) => const RentalPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/masters/vendors',
|
||||||
|
name: 'masters-vendors',
|
||||||
|
builder: (context, state) => const VendorPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/masters/products',
|
||||||
|
name: 'masters-products',
|
||||||
|
builder: (context, state) => const ProductPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/masters/warehouses',
|
||||||
|
name: 'masters-warehouses',
|
||||||
|
builder: (context, state) => const WarehousePage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/masters/customers',
|
||||||
|
name: 'masters-customers',
|
||||||
|
builder: (context, state) => const CustomerPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/masters/users',
|
||||||
|
name: 'masters-users',
|
||||||
|
builder: (context, state) => const UserPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/masters/groups',
|
||||||
|
name: 'masters-groups',
|
||||||
|
builder: (context, state) => const GroupPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/masters/menus',
|
||||||
|
name: 'masters-menus',
|
||||||
|
builder: (context, state) => const MenuPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/masters/group-permissions',
|
||||||
|
name: 'masters-group-permissions',
|
||||||
|
builder: (context, state) => const GroupPermissionPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/approvals/requests',
|
||||||
|
name: 'approvals-requests',
|
||||||
|
builder: (context, state) => const ApprovalRequestPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/approvals/steps',
|
||||||
|
name: 'approvals-steps',
|
||||||
|
builder: (context, state) => const ApprovalStepPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/approvals/history',
|
||||||
|
name: 'approvals-history',
|
||||||
|
builder: (context, state) => const ApprovalHistoryPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/approvals/templates',
|
||||||
|
name: 'approvals-templates',
|
||||||
|
builder: (context, state) => const ApprovalTemplatePage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/utilities/postal-search',
|
||||||
|
name: 'utilities-postal-search',
|
||||||
|
builder: (context, state) => const PostalSearchPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/reports',
|
||||||
|
name: 'reports',
|
||||||
|
builder: (context, state) => const ReportingPage(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class ApprovalHistoryPage extends StatelessWidget {
|
||||||
|
const ApprovalHistoryPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '결재 이력 조회',
|
||||||
|
summary: '결재 단계별 변경 이력을 조회합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '조회 테이블',
|
||||||
|
description: '수정 없이 이력 리스트만 제공.',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: [
|
||||||
|
'번호',
|
||||||
|
'결재ID',
|
||||||
|
'단계ID',
|
||||||
|
'승인자',
|
||||||
|
'행위',
|
||||||
|
'변경전상태',
|
||||||
|
'변경후상태',
|
||||||
|
'작업일시',
|
||||||
|
'비고',
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'APP-20240301-001',
|
||||||
|
'STEP-1',
|
||||||
|
'최관리',
|
||||||
|
'승인',
|
||||||
|
'승인대기',
|
||||||
|
'승인완료',
|
||||||
|
'2024-03-01 10:30',
|
||||||
|
'-',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class ApprovalRequestPage extends StatelessWidget {
|
||||||
|
const ApprovalRequestPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '결재 관리',
|
||||||
|
summary: '결재 번호와 상태, 상신자를 확인하고 결재 플로우를 제어합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'트랜잭션번호 [Dropdown]',
|
||||||
|
'결재번호 [자동생성]',
|
||||||
|
'결재상태 [Dropdown]',
|
||||||
|
'상신자 [자동]',
|
||||||
|
'비고 [Text]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '수정 폼',
|
||||||
|
items: ['결재번호 [ReadOnly]', '상신자 [ReadOnly]', '요청일시 [ReadOnly]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: [
|
||||||
|
'번호',
|
||||||
|
'결재번호',
|
||||||
|
'트랜잭션번호',
|
||||||
|
'상태',
|
||||||
|
'상신자',
|
||||||
|
'요청일시',
|
||||||
|
'최종결정일시',
|
||||||
|
'비고',
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'APP-20240301-001',
|
||||||
|
'IN-20240301-001',
|
||||||
|
'승인대기',
|
||||||
|
'홍길동',
|
||||||
|
'2024-03-01 09:00',
|
||||||
|
'-',
|
||||||
|
'-',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class ApprovalStepPage extends StatelessWidget {
|
||||||
|
const ApprovalStepPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '결재 단계 관리',
|
||||||
|
summary: '결재 단계 순서와 승인자를 구성합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'결재ID [Dropdown]',
|
||||||
|
'단계순서 [Number]',
|
||||||
|
'승인자 [Dropdown]',
|
||||||
|
'단계상태 [Dropdown]',
|
||||||
|
'비고 [Text]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '수정 폼',
|
||||||
|
items: ['결재ID [ReadOnly]', '단계순서 [ReadOnly]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: ['번호', '결재ID', '단계순서', '승인자', '상태', '배정일시', '결정일시', '비고'],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'APP-20240301-001',
|
||||||
|
'1',
|
||||||
|
'최관리',
|
||||||
|
'승인대기',
|
||||||
|
'2024-03-01 09:00',
|
||||||
|
'-',
|
||||||
|
'-',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class ApprovalTemplatePage extends StatelessWidget {
|
||||||
|
const ApprovalTemplatePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '결재 템플릿 관리',
|
||||||
|
summary: '반복적인 결재 흐름을 템플릿으로 정의합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'템플릿코드 [Text]',
|
||||||
|
'템플릿명 [Text]',
|
||||||
|
'설명 [Text]',
|
||||||
|
'작성자 [ReadOnly]',
|
||||||
|
'사용여부 [Switch]',
|
||||||
|
'비고 [Text]',
|
||||||
|
'단계 추가: 순서 [Number], 승인자 [Dropdown]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '수정 폼',
|
||||||
|
items: ['템플릿코드 [ReadOnly]', '작성자 [ReadOnly]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: ['번호', '템플릿코드', '템플릿명', '설명', '작성자', '사용여부', '변경일시'],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'TEMP-001',
|
||||||
|
'입고 기본 결재',
|
||||||
|
'입고 처리 2단계 결재',
|
||||||
|
'홍길동',
|
||||||
|
'Y',
|
||||||
|
'2024-03-01 10:00',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class DashboardPage extends StatelessWidget {
|
||||||
|
const DashboardPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '대시보드',
|
||||||
|
summary: '오늘 입고/출고, 결재 대기, 최근 트랜잭션을 한 눈에 볼 수 있는 메인 화면 구성.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '주요 위젯',
|
||||||
|
items: [
|
||||||
|
'오늘 입고/출고 건수, 대기 결재 수 KPI 카드',
|
||||||
|
'최근 트랜잭션 리스트: 번호 · 일자 · 유형 · 상태 · 작성자',
|
||||||
|
'내 결재 요청/대기 건 알림 패널',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,960 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
|
class InboundPage extends StatefulWidget {
|
||||||
|
const InboundPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InboundPage> createState() => _InboundPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InboundPageState extends State<InboundPage> {
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final DateFormat _dateFormatter = DateFormat('yyyy-MM-dd');
|
||||||
|
final NumberFormat _currencyFormatter = NumberFormat.currency(
|
||||||
|
locale: 'ko_KR',
|
||||||
|
symbol: '₩',
|
||||||
|
decimalDigits: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
DateTimeRange? _dateRange;
|
||||||
|
final List<InboundRecord> _records = _mockRecords;
|
||||||
|
InboundRecord? _selectedRecord;
|
||||||
|
|
||||||
|
static const _statusOptions = ['작성중', '승인대기', '승인완료'];
|
||||||
|
static const _warehouseOptions = ['서울 1창고', '부산 센터', '대전 물류'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (_records.isNotEmpty) {
|
||||||
|
_selectedRecord = _records.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = ShadTheme.of(context);
|
||||||
|
final filtered = _filteredRecords;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('입고 관리', style: theme.textTheme.h2),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'입고 처리, 라인 품목, 상태를 한 화면에서 확인하고 관리합니다.',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ShadButton(
|
||||||
|
leading: const Icon(LucideIcons.plus, size: 16),
|
||||||
|
onPressed: _handleCreate,
|
||||||
|
child: const Text('입고 등록'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ShadButton.outline(
|
||||||
|
leading: const Icon(LucideIcons.pencil, size: 16),
|
||||||
|
onPressed: _selectedRecord == null
|
||||||
|
? null
|
||||||
|
: () => _handleEdit(_selectedRecord!),
|
||||||
|
child: const Text('선택 항목 수정'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ShadCard(
|
||||||
|
title: Text('검색 필터', style: theme.textTheme.h3),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 260,
|
||||||
|
child: ShadInput(
|
||||||
|
controller: _searchController,
|
||||||
|
placeholder: const Text('트랜잭션번호, 작성자, 제품 검색'),
|
||||||
|
leading: const Icon(LucideIcons.search, size: 16),
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 220,
|
||||||
|
child: ShadButton.outline(
|
||||||
|
onPressed: _pickDateRange,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(LucideIcons.calendar, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
_dateRange == null
|
||||||
|
? '기간 선택'
|
||||||
|
: '${_dateFormatter.format(_dateRange!.start)} ~ ${_dateFormatter.format(_dateRange!.end)}',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_dateRange != null)
|
||||||
|
ShadButton.ghost(
|
||||||
|
onPressed: () => setState(() => _dateRange = null),
|
||||||
|
child: const Text('기간 초기화'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ShadCard(
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('입고 내역', style: theme.textTheme.h3),
|
||||||
|
Text('${filtered.length}건', style: theme.textTheme.muted),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 420,
|
||||||
|
child: filtered.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
'조건에 맞는 입고 내역이 없습니다.',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ShadTable.list(
|
||||||
|
header: _tableHeaders
|
||||||
|
.map(
|
||||||
|
(header) =>
|
||||||
|
ShadTableCell.header(child: Text(header)),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
children: [
|
||||||
|
for (final record in filtered)
|
||||||
|
_buildRecordRow(record).map(
|
||||||
|
(value) => ShadTableCell(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
columnSpanExtent: (index) =>
|
||||||
|
const FixedTableSpanExtent(140),
|
||||||
|
rowSpanExtent: (index) => const FixedTableSpanExtent(56),
|
||||||
|
onRowTap: (rowIndex) {
|
||||||
|
setState(() {
|
||||||
|
_selectedRecord = filtered[rowIndex];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_selectedRecord != null) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_DetailCard(
|
||||||
|
record: _selectedRecord!,
|
||||||
|
dateFormatter: _dateFormatter,
|
||||||
|
currencyFormatter: _currencyFormatter,
|
||||||
|
onEdit: () => _handleEdit(_selectedRecord!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<InboundRecord> get _filteredRecords {
|
||||||
|
final query = _searchController.text.trim().toLowerCase();
|
||||||
|
return _records.where((record) {
|
||||||
|
final matchesQuery =
|
||||||
|
query.isEmpty ||
|
||||||
|
record.number.toLowerCase().contains(query) ||
|
||||||
|
record.transactionNumber.toLowerCase().contains(query) ||
|
||||||
|
record.writer.toLowerCase().contains(query) ||
|
||||||
|
record.items.any(
|
||||||
|
(item) => item.product.toLowerCase().contains(query),
|
||||||
|
);
|
||||||
|
final matchesRange =
|
||||||
|
_dateRange == null ||
|
||||||
|
(!record.processedAt.isBefore(_dateRange!.start) &&
|
||||||
|
!record.processedAt.isAfter(_dateRange!.end));
|
||||||
|
return matchesQuery && matchesRange;
|
||||||
|
}).toList()..sort((a, b) => b.processedAt.compareTo(a.processedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _buildRecordRow(InboundRecord record) {
|
||||||
|
final primaryItem = record.items.first;
|
||||||
|
return [
|
||||||
|
record.number.split('-').last,
|
||||||
|
_dateFormatter.format(record.processedAt),
|
||||||
|
record.warehouse,
|
||||||
|
record.transactionNumber,
|
||||||
|
primaryItem.product,
|
||||||
|
primaryItem.manufacturer,
|
||||||
|
primaryItem.unit,
|
||||||
|
record.totalQuantity.toString(),
|
||||||
|
_currencyFormatter.format(primaryItem.price),
|
||||||
|
record.status,
|
||||||
|
record.writer,
|
||||||
|
record.itemCount.toString(),
|
||||||
|
record.totalQuantity.toString(),
|
||||||
|
record.remark.isEmpty ? '-' : record.remark,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickDateRange() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final range = await showDateRangePicker(
|
||||||
|
context: context,
|
||||||
|
firstDate: DateTime(now.year - 5),
|
||||||
|
lastDate: DateTime(now.year + 5),
|
||||||
|
initialDateRange: _dateRange,
|
||||||
|
);
|
||||||
|
if (range != null) {
|
||||||
|
setState(() => _dateRange = range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleCreate() async {
|
||||||
|
final record = await _showInboundFormDialog();
|
||||||
|
if (record != null) {
|
||||||
|
setState(() {
|
||||||
|
_records.insert(0, record);
|
||||||
|
_selectedRecord = record;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleEdit(InboundRecord record) async {
|
||||||
|
final updated = await _showInboundFormDialog(initial: record);
|
||||||
|
if (updated != null) {
|
||||||
|
setState(() {
|
||||||
|
final index = _records.indexWhere(
|
||||||
|
(element) => element.number == record.number,
|
||||||
|
);
|
||||||
|
if (index != -1) {
|
||||||
|
_records[index] = updated;
|
||||||
|
_selectedRecord = updated;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<InboundRecord?> _showInboundFormDialog({
|
||||||
|
InboundRecord? initial,
|
||||||
|
}) async {
|
||||||
|
final processedAt = ValueNotifier<DateTime>(
|
||||||
|
initial?.processedAt ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
final warehouseController = TextEditingController(
|
||||||
|
text: initial?.warehouse ?? _warehouseOptions.first,
|
||||||
|
);
|
||||||
|
final statusValue = ValueNotifier<String>(
|
||||||
|
initial?.status ?? _statusOptions.first,
|
||||||
|
);
|
||||||
|
final writerController = TextEditingController(
|
||||||
|
text: initial?.writer ?? '홍길동',
|
||||||
|
);
|
||||||
|
final remarkController = TextEditingController(text: initial?.remark ?? '');
|
||||||
|
|
||||||
|
final drafts =
|
||||||
|
initial?.items
|
||||||
|
.map((item) => _LineItemDraft.fromItem(item))
|
||||||
|
.toList()
|
||||||
|
.cast<_LineItemDraft>() ??
|
||||||
|
[_LineItemDraft.empty()];
|
||||||
|
|
||||||
|
InboundRecord? result;
|
||||||
|
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
final theme = ShadTheme.of(dialogContext);
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
return Dialog(
|
||||||
|
insetPadding: const EdgeInsets.all(24),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 860,
|
||||||
|
maxHeight: 720,
|
||||||
|
),
|
||||||
|
child: ShadCard(
|
||||||
|
title: Text(
|
||||||
|
initial == null ? '입고 등록' : '입고 수정',
|
||||||
|
style: theme.textTheme.h3,
|
||||||
|
),
|
||||||
|
description: Text(
|
||||||
|
'입고 기본정보와 품목 라인을 입력하세요.',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
),
|
||||||
|
footer: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
ShadButton.ghost(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('취소'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ShadButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (drafts.any(
|
||||||
|
(draft) => draft.product.text.isEmpty,
|
||||||
|
)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('품목 정보를 입력하세요.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final items = drafts
|
||||||
|
.map(
|
||||||
|
(draft) => InboundLineItem(
|
||||||
|
product: draft.product.text,
|
||||||
|
manufacturer: draft.manufacturer.text,
|
||||||
|
unit: draft.unit.text,
|
||||||
|
quantity:
|
||||||
|
int.tryParse(draft.quantity.text) ?? 0,
|
||||||
|
price:
|
||||||
|
double.tryParse(
|
||||||
|
draft.price.text.replaceAll(',', ''),
|
||||||
|
) ??
|
||||||
|
0,
|
||||||
|
remark: draft.remark.text,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
final record = InboundRecord(
|
||||||
|
number:
|
||||||
|
initial?.number ??
|
||||||
|
_generateInboundNumber(processedAt.value),
|
||||||
|
transactionNumber:
|
||||||
|
initial?.transactionNumber ??
|
||||||
|
_generateTransactionNumber(processedAt.value),
|
||||||
|
processedAt: processedAt.value,
|
||||||
|
warehouse: warehouseController.text,
|
||||||
|
status: statusValue.value,
|
||||||
|
writer: writerController.text,
|
||||||
|
remark: remarkController.text,
|
||||||
|
items: items,
|
||||||
|
);
|
||||||
|
result = record;
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
child: const Text('저장'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(right: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 240,
|
||||||
|
child: _FormFieldLabel(
|
||||||
|
label: '처리일자',
|
||||||
|
child: ShadButton.outline(
|
||||||
|
onPressed: () async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: processedAt.value,
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime(2030),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
processedAt.value = picked;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_dateFormatter.format(
|
||||||
|
processedAt.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
LucideIcons.calendar,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 240,
|
||||||
|
child: _FormFieldLabel(
|
||||||
|
label: '창고',
|
||||||
|
child: ShadSelect<String>(
|
||||||
|
initialValue: warehouseController.text,
|
||||||
|
selectedOptionBuilder: (context, value) =>
|
||||||
|
Text(value),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
warehouseController.text = value;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: _warehouseOptions
|
||||||
|
.map(
|
||||||
|
(option) => ShadOption(
|
||||||
|
value: option,
|
||||||
|
child: Text(option),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 240,
|
||||||
|
child: _FormFieldLabel(
|
||||||
|
label: '상태',
|
||||||
|
child: ShadSelect<String>(
|
||||||
|
initialValue: statusValue.value,
|
||||||
|
selectedOptionBuilder: (context, value) =>
|
||||||
|
Text(value),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
statusValue.value = value;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: _statusOptions
|
||||||
|
.map(
|
||||||
|
(status) => ShadOption(
|
||||||
|
value: status,
|
||||||
|
child: Text(status),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 240,
|
||||||
|
child: _FormFieldLabel(
|
||||||
|
label: '작성자',
|
||||||
|
child: ShadInput(controller: writerController),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 500,
|
||||||
|
child: _FormFieldLabel(
|
||||||
|
label: '비고',
|
||||||
|
child: ShadInput(
|
||||||
|
controller: remarkController,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('라인 품목', style: theme.textTheme.h4),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'제품, 제조사, 단위, 수량, 단가 정보를 입력하세요.',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ShadButton.outline(
|
||||||
|
leading: const Icon(LucideIcons.plus, size: 16),
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
drafts.add(_LineItemDraft.empty());
|
||||||
|
}),
|
||||||
|
child: const Text('품목 추가'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
for (final draft in drafts)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: _LineItemRow(
|
||||||
|
draft: draft,
|
||||||
|
onRemove: drafts.length == 1
|
||||||
|
? null
|
||||||
|
: () => setState(() {
|
||||||
|
draft.dispose();
|
||||||
|
drafts.remove(draft);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final draft in drafts) {
|
||||||
|
draft.dispose();
|
||||||
|
}
|
||||||
|
warehouseController.dispose();
|
||||||
|
statusValue.dispose();
|
||||||
|
writerController.dispose();
|
||||||
|
remarkController.dispose();
|
||||||
|
processedAt.dispose();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _generateInboundNumber(DateTime date) {
|
||||||
|
final stamp = DateFormat('yyyyMMdd-HHmmss').format(date);
|
||||||
|
return 'IN-$stamp';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _generateTransactionNumber(DateTime date) {
|
||||||
|
final stamp = DateFormat('yyyyMMdd-HHmmss').format(date);
|
||||||
|
return 'TX-$stamp';
|
||||||
|
}
|
||||||
|
|
||||||
|
static const _tableHeaders = [
|
||||||
|
'번호',
|
||||||
|
'처리일자',
|
||||||
|
'창고',
|
||||||
|
'트랜잭션번호',
|
||||||
|
'제품',
|
||||||
|
'제조사',
|
||||||
|
'단위',
|
||||||
|
'수량',
|
||||||
|
'단가',
|
||||||
|
'상태',
|
||||||
|
'작성자',
|
||||||
|
'품목수',
|
||||||
|
'총수량',
|
||||||
|
'비고',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DetailCard extends StatelessWidget {
|
||||||
|
const _DetailCard({
|
||||||
|
required this.record,
|
||||||
|
required this.dateFormatter,
|
||||||
|
required this.currencyFormatter,
|
||||||
|
required this.onEdit,
|
||||||
|
});
|
||||||
|
|
||||||
|
final InboundRecord record;
|
||||||
|
final DateFormat dateFormatter;
|
||||||
|
final NumberFormat currencyFormatter;
|
||||||
|
final VoidCallback onEdit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = ShadTheme.of(context);
|
||||||
|
|
||||||
|
return ShadCard(
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('선택된 입고 상세', style: theme.textTheme.h3),
|
||||||
|
ShadButton.outline(
|
||||||
|
leading: const Icon(LucideIcons.pencil, size: 16),
|
||||||
|
onPressed: onEdit,
|
||||||
|
child: const Text('수정'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
description: Text(
|
||||||
|
'트랜잭션번호 ${record.transactionNumber}',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
_DetailChip(
|
||||||
|
label: '처리일자',
|
||||||
|
value: dateFormatter.format(record.processedAt),
|
||||||
|
),
|
||||||
|
_DetailChip(label: '창고', value: record.warehouse),
|
||||||
|
_DetailChip(label: '상태', value: record.status),
|
||||||
|
_DetailChip(label: '작성자', value: record.writer),
|
||||||
|
_DetailChip(label: '품목 수', value: '${record.itemCount}'),
|
||||||
|
_DetailChip(label: '총 수량', value: '${record.totalQuantity}'),
|
||||||
|
_DetailChip(
|
||||||
|
label: '총 금액',
|
||||||
|
value: currencyFormatter.format(record.totalAmount),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text('라인 품목', style: theme.textTheme.h4),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: (record.items.length * 52).clamp(160, 260).toDouble(),
|
||||||
|
child: ShadTable.list(
|
||||||
|
header: const [
|
||||||
|
ShadTableCell.header(child: Text('제품')),
|
||||||
|
ShadTableCell.header(child: Text('제조사')),
|
||||||
|
ShadTableCell.header(child: Text('단위')),
|
||||||
|
ShadTableCell.header(child: Text('수량')),
|
||||||
|
ShadTableCell.header(child: Text('단가')),
|
||||||
|
ShadTableCell.header(child: Text('비고')),
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
for (final item in record.items)
|
||||||
|
[
|
||||||
|
ShadTableCell(child: Text(item.product)),
|
||||||
|
ShadTableCell(child: Text(item.manufacturer)),
|
||||||
|
ShadTableCell(child: Text(item.unit)),
|
||||||
|
ShadTableCell(child: Text('${item.quantity}')),
|
||||||
|
ShadTableCell(
|
||||||
|
child: Text(currencyFormatter.format(item.price)),
|
||||||
|
),
|
||||||
|
ShadTableCell(
|
||||||
|
child: Text(item.remark.isEmpty ? '-' : item.remark),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
columnSpanExtent: (index) => const FixedTableSpanExtent(136),
|
||||||
|
rowSpanExtent: (index) => const FixedTableSpanExtent(52),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DetailChip extends StatelessWidget {
|
||||||
|
const _DetailChip({required this.label, required this.value});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = ShadTheme.of(context);
|
||||||
|
return ShadBadge.outline(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(label, style: theme.textTheme.small),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(value, style: theme.textTheme.p),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FormFieldLabel extends StatelessWidget {
|
||||||
|
const _FormFieldLabel({required this.label, required this.child});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = ShadTheme.of(context);
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label, style: theme.textTheme.small),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LineItemRow extends StatelessWidget {
|
||||||
|
const _LineItemRow({required this.draft, required this.onRemove});
|
||||||
|
|
||||||
|
final _LineItemDraft draft;
|
||||||
|
final VoidCallback? onRemove;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ShadInput(
|
||||||
|
controller: draft.product,
|
||||||
|
placeholder: const Text('제품명'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: ShadInput(
|
||||||
|
controller: draft.manufacturer,
|
||||||
|
placeholder: const Text('제조사'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: ShadInput(
|
||||||
|
controller: draft.unit,
|
||||||
|
placeholder: const Text('단위'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: ShadInput(
|
||||||
|
controller: draft.quantity,
|
||||||
|
placeholder: const Text('수량'),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: ShadInput(
|
||||||
|
controller: draft.price,
|
||||||
|
placeholder: const Text('단가'),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: ShadInput(
|
||||||
|
controller: draft.remark,
|
||||||
|
placeholder: const Text('비고'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ShadButton.ghost(
|
||||||
|
size: ShadButtonSize.sm,
|
||||||
|
onPressed: onRemove,
|
||||||
|
child: const Icon(LucideIcons.trash2, size: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LineItemDraft {
|
||||||
|
_LineItemDraft._({
|
||||||
|
required this.product,
|
||||||
|
required this.manufacturer,
|
||||||
|
required this.unit,
|
||||||
|
required this.quantity,
|
||||||
|
required this.price,
|
||||||
|
required this.remark,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController product;
|
||||||
|
final TextEditingController manufacturer;
|
||||||
|
final TextEditingController unit;
|
||||||
|
final TextEditingController quantity;
|
||||||
|
final TextEditingController price;
|
||||||
|
final TextEditingController remark;
|
||||||
|
|
||||||
|
factory _LineItemDraft.empty() {
|
||||||
|
return _LineItemDraft._(
|
||||||
|
product: TextEditingController(),
|
||||||
|
manufacturer: TextEditingController(),
|
||||||
|
unit: TextEditingController(text: 'EA'),
|
||||||
|
quantity: TextEditingController(text: '0'),
|
||||||
|
price: TextEditingController(text: '0'),
|
||||||
|
remark: TextEditingController(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory _LineItemDraft.fromItem(InboundLineItem item) {
|
||||||
|
return _LineItemDraft._(
|
||||||
|
product: TextEditingController(text: item.product),
|
||||||
|
manufacturer: TextEditingController(text: item.manufacturer),
|
||||||
|
unit: TextEditingController(text: item.unit),
|
||||||
|
quantity: TextEditingController(text: '${item.quantity}'),
|
||||||
|
price: TextEditingController(text: item.price.toStringAsFixed(0)),
|
||||||
|
remark: TextEditingController(text: item.remark),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
product.dispose();
|
||||||
|
manufacturer.dispose();
|
||||||
|
unit.dispose();
|
||||||
|
quantity.dispose();
|
||||||
|
price.dispose();
|
||||||
|
remark.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InboundRecord {
|
||||||
|
InboundRecord({
|
||||||
|
required this.number,
|
||||||
|
required this.transactionNumber,
|
||||||
|
required this.processedAt,
|
||||||
|
required this.warehouse,
|
||||||
|
required this.status,
|
||||||
|
required this.writer,
|
||||||
|
required this.remark,
|
||||||
|
required this.items,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String number;
|
||||||
|
final String transactionNumber;
|
||||||
|
final DateTime processedAt;
|
||||||
|
final String warehouse;
|
||||||
|
final String status;
|
||||||
|
final String writer;
|
||||||
|
final String remark;
|
||||||
|
final List<InboundLineItem> items;
|
||||||
|
|
||||||
|
int get itemCount => items.length;
|
||||||
|
int get totalQuantity =>
|
||||||
|
items.fold<int>(0, (sum, item) => sum + item.quantity);
|
||||||
|
double get totalAmount =>
|
||||||
|
items.fold<double>(0, (sum, item) => sum + (item.price * item.quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
class InboundLineItem {
|
||||||
|
InboundLineItem({
|
||||||
|
required this.product,
|
||||||
|
required this.manufacturer,
|
||||||
|
required this.unit,
|
||||||
|
required this.quantity,
|
||||||
|
required this.price,
|
||||||
|
required this.remark,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String product;
|
||||||
|
final String manufacturer;
|
||||||
|
final String unit;
|
||||||
|
final int quantity;
|
||||||
|
final double price;
|
||||||
|
final String remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<InboundRecord> _mockRecords = [
|
||||||
|
InboundRecord(
|
||||||
|
number: 'IN-20240301-001',
|
||||||
|
transactionNumber: 'TX-20240301-001',
|
||||||
|
processedAt: DateTime(2024, 3, 1),
|
||||||
|
warehouse: '서울 1창고',
|
||||||
|
status: '작성중',
|
||||||
|
writer: '홍길동',
|
||||||
|
remark: '-',
|
||||||
|
items: [
|
||||||
|
InboundLineItem(
|
||||||
|
product: 'XR-5000',
|
||||||
|
manufacturer: '슈퍼벤더',
|
||||||
|
unit: 'EA',
|
||||||
|
quantity: 40,
|
||||||
|
price: 120000,
|
||||||
|
remark: '',
|
||||||
|
),
|
||||||
|
InboundLineItem(
|
||||||
|
product: 'XR-5001',
|
||||||
|
manufacturer: '슈퍼벤더',
|
||||||
|
unit: 'EA',
|
||||||
|
quantity: 60,
|
||||||
|
price: 98000,
|
||||||
|
remark: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
InboundRecord(
|
||||||
|
number: 'IN-20240305-002',
|
||||||
|
transactionNumber: 'TX-20240305-010',
|
||||||
|
processedAt: DateTime(2024, 3, 5),
|
||||||
|
warehouse: '부산 센터',
|
||||||
|
status: '승인대기',
|
||||||
|
writer: '김담당',
|
||||||
|
remark: '긴급 입고',
|
||||||
|
items: [
|
||||||
|
InboundLineItem(
|
||||||
|
product: 'Eco-200',
|
||||||
|
manufacturer: '그린텍',
|
||||||
|
unit: 'EA',
|
||||||
|
quantity: 25,
|
||||||
|
price: 145000,
|
||||||
|
remark: 'QC 필요',
|
||||||
|
),
|
||||||
|
InboundLineItem(
|
||||||
|
product: 'Eco-200B',
|
||||||
|
manufacturer: '그린텍',
|
||||||
|
unit: 'EA',
|
||||||
|
quantity: 10,
|
||||||
|
price: 160000,
|
||||||
|
remark: '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
InboundRecord(
|
||||||
|
number: 'IN-20240310-003',
|
||||||
|
transactionNumber: 'TX-20240310-004',
|
||||||
|
processedAt: DateTime(2024, 3, 10),
|
||||||
|
warehouse: '대전 물류',
|
||||||
|
status: '승인완료',
|
||||||
|
writer: '최검수',
|
||||||
|
remark: '완료',
|
||||||
|
items: [
|
||||||
|
InboundLineItem(
|
||||||
|
product: 'Delta-One',
|
||||||
|
manufacturer: '델타',
|
||||||
|
unit: 'SET',
|
||||||
|
quantity: 8,
|
||||||
|
price: 450000,
|
||||||
|
remark: '설치 일정 확인',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
1125
lib/features/inventory/rental/presentation/pages/rental_page.dart
Normal file
88
lib/features/login/presentation/pages/login_page.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
|
import '../../../../core/constants/app_sections.dart';
|
||||||
|
|
||||||
|
class LoginPage extends StatefulWidget {
|
||||||
|
const LoginPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoginPage> createState() => _LoginPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPageState extends State<LoginPage> {
|
||||||
|
final idController = TextEditingController();
|
||||||
|
final passwordController = TextEditingController();
|
||||||
|
bool rememberMe = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
idController.dispose();
|
||||||
|
passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSubmit() {
|
||||||
|
context.go(dashboardRoutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = ShadTheme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 460),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: ShadCard(
|
||||||
|
title: Text('Superport v2 로그인', style: theme.textTheme.h3),
|
||||||
|
description: Text(
|
||||||
|
'사번 또는 이메일과 비밀번호를 입력하여 대시보드로 이동합니다.',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
ShadInput(
|
||||||
|
controller: idController,
|
||||||
|
placeholder: const Text('사번 또는 이메일'),
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
leading: const Icon(LucideIcons.user),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ShadInput(
|
||||||
|
controller: passwordController,
|
||||||
|
placeholder: const Text('비밀번호'),
|
||||||
|
obscureText: true,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
leading: const Icon(LucideIcons.lock),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ShadSwitch(
|
||||||
|
value: rememberMe,
|
||||||
|
onChanged: (value) =>
|
||||||
|
setState(() => rememberMe = value),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text('자동 로그인', style: theme.textTheme.small),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ShadButton(
|
||||||
|
onPressed: _handleSubmit,
|
||||||
|
child: const Text('로그인'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class CustomerPage extends StatelessWidget {
|
||||||
|
const CustomerPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '회사(고객사) 관리',
|
||||||
|
summary: '고객사 기본 정보와 연락처, 주소를 관리합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'고객사코드 [Text]',
|
||||||
|
'고객사명 [Text]',
|
||||||
|
'유형 (파트너/일반) [Dropdown]',
|
||||||
|
'이메일 [Text]',
|
||||||
|
'연락처 [Text]',
|
||||||
|
'우편번호 [검색 연동], 상세주소 [Text]',
|
||||||
|
'사용여부 [Switch]',
|
||||||
|
'비고 [Text]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '수정 폼',
|
||||||
|
items: ['고객사코드 [ReadOnly]', '생성일시 [ReadOnly]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: [
|
||||||
|
'번호',
|
||||||
|
'고객사코드',
|
||||||
|
'고객사명',
|
||||||
|
'유형',
|
||||||
|
'이메일',
|
||||||
|
'연락처',
|
||||||
|
'우편번호',
|
||||||
|
'상세주소',
|
||||||
|
'사용여부',
|
||||||
|
'비고',
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'C-001',
|
||||||
|
'슈퍼포트 파트너',
|
||||||
|
'파트너',
|
||||||
|
'partner@superport.com',
|
||||||
|
'02-1234-5678',
|
||||||
|
'04532',
|
||||||
|
'서울시 중구 을지로 100',
|
||||||
|
'Y',
|
||||||
|
'-',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class GroupPage extends StatelessWidget {
|
||||||
|
const GroupPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '그룹 관리',
|
||||||
|
summary: '권한 그룹 정의와 기본여부 설정을 제공합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'그룹명 [Text]',
|
||||||
|
'그룹설명 [Text]',
|
||||||
|
'기본여부 [Switch]',
|
||||||
|
'사용여부 [Switch]',
|
||||||
|
'비고 [Text]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '수정 폼',
|
||||||
|
items: ['그룹명 [ReadOnly]', '생성일시 [ReadOnly]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: ['번호', '그룹명', '설명', '기본여부', '사용여부', '비고', '변경일시'],
|
||||||
|
rows: [
|
||||||
|
['1', '관리자', '시스템 전체 권한', 'Y', 'Y', '-', '2024-03-01 10:00'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class GroupPermissionPage extends StatelessWidget {
|
||||||
|
const GroupPermissionPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '그룹 메뉴 권한 관리',
|
||||||
|
summary: '그룹별 메뉴 접근과 CRUD 권한을 설정합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'그룹 [Dropdown]',
|
||||||
|
'메뉴 [Dropdown]',
|
||||||
|
'생성권한 [Checkbox]',
|
||||||
|
'조회권한 [Checkbox]',
|
||||||
|
'수정권한 [Checkbox]',
|
||||||
|
'삭제권한 [Checkbox]',
|
||||||
|
'사용여부 [Switch]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(title: '수정 폼', items: ['그룹 [ReadOnly]', '메뉴 [ReadOnly]']),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: [
|
||||||
|
'번호',
|
||||||
|
'그룹명',
|
||||||
|
'메뉴명',
|
||||||
|
'생성',
|
||||||
|
'조회',
|
||||||
|
'수정',
|
||||||
|
'삭제',
|
||||||
|
'사용여부',
|
||||||
|
'변경일시',
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
['1', '관리자', '대시보드', 'Y', 'Y', 'Y', 'Y', 'Y', '2024-03-01 10:00'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
lib/features/masters/menu/presentation/pages/menu_page.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class MenuPage extends StatelessWidget {
|
||||||
|
const MenuPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '메뉴 관리',
|
||||||
|
summary: '메뉴 계층, 경로, 노출 순서를 구성합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'메뉴코드 [Text]',
|
||||||
|
'메뉴명 [Text]',
|
||||||
|
'상위메뉴 [Dropdown]',
|
||||||
|
'경로 [Text]',
|
||||||
|
'표시순서 [Number]',
|
||||||
|
'사용여부 [Switch]',
|
||||||
|
'비고 [Text]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '수정 폼',
|
||||||
|
items: ['메뉴코드 [ReadOnly]', '생성일시 [ReadOnly]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: ['번호', '메뉴코드', '메뉴명', '상위메뉴', '경로', '사용여부', '비고', '변경일시'],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'MN-001',
|
||||||
|
'대시보드',
|
||||||
|
'-',
|
||||||
|
'/dashboard',
|
||||||
|
'Y',
|
||||||
|
'-',
|
||||||
|
'2024-03-01 10:00',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class ProductPage extends StatelessWidget {
|
||||||
|
const ProductPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '장비 모델(제품) 관리',
|
||||||
|
summary: '제품 코드, 제조사, 단위 정보를 유지하여 재고 라인과 연계합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'제품코드 [Text]',
|
||||||
|
'제품명 [Text]',
|
||||||
|
'제조사 [Dropdown]',
|
||||||
|
'단위 [Dropdown]',
|
||||||
|
'사용여부 [Switch]',
|
||||||
|
'비고 [Text]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '수정 폼',
|
||||||
|
items: ['제품코드 [ReadOnly]', '생성일시 [ReadOnly]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: ['번호', '제품코드', '제품명', '제조사', '단위', '사용여부', '비고', '변경일시'],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'P-100',
|
||||||
|
'XR-5000',
|
||||||
|
'슈퍼벤더',
|
||||||
|
'EA',
|
||||||
|
'Y',
|
||||||
|
'-',
|
||||||
|
'2024-03-01 10:00',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
lib/features/masters/user/presentation/pages/user_page.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class UserPage extends StatelessWidget {
|
||||||
|
const UserPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '사용자(사원) 관리',
|
||||||
|
summary: '사번 기반 계정과 그룹, 사용 상태를 관리합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'사번 [Text]',
|
||||||
|
'성명 [Text]',
|
||||||
|
'이메일 [Text]',
|
||||||
|
'연락처 [Text]',
|
||||||
|
'그룹 [Dropdown]',
|
||||||
|
'사용여부 [Switch]',
|
||||||
|
'비고 [Text]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(title: '수정 폼', items: ['사번 [ReadOnly]', '생성일시 [ReadOnly]']),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: [
|
||||||
|
'번호',
|
||||||
|
'사번',
|
||||||
|
'성명',
|
||||||
|
'이메일',
|
||||||
|
'연락처',
|
||||||
|
'그룹',
|
||||||
|
'사용여부',
|
||||||
|
'비고',
|
||||||
|
'변경일시',
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'A0001',
|
||||||
|
'김철수',
|
||||||
|
'kim@superport.com',
|
||||||
|
'010-1111-2222',
|
||||||
|
'관리자',
|
||||||
|
'Y',
|
||||||
|
'-',
|
||||||
|
'2024-03-01 10:00',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/features/masters/vendor/data/dtos/vendor_dto.dart
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import '../../domain/entities/vendor.dart';
|
||||||
|
|
||||||
|
/// 벤더 DTO (JSON 직렬화/역직렬화)
|
||||||
|
class VendorDto {
|
||||||
|
VendorDto({
|
||||||
|
this.id,
|
||||||
|
required this.vendorCode,
|
||||||
|
required this.vendorName,
|
||||||
|
this.isActive = true,
|
||||||
|
this.isDeleted = false,
|
||||||
|
this.note,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int? id;
|
||||||
|
final String vendorCode;
|
||||||
|
final String vendorName;
|
||||||
|
final bool isActive;
|
||||||
|
final bool isDeleted;
|
||||||
|
final String? note;
|
||||||
|
final DateTime? createdAt;
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
factory VendorDto.fromJson(Map<String, dynamic> json) {
|
||||||
|
return VendorDto(
|
||||||
|
id: json['id'] as int?,
|
||||||
|
vendorCode: json['vendor_code'] as String,
|
||||||
|
vendorName: json['vendor_name'] as String,
|
||||||
|
isActive: (json['is_active'] as bool?) ?? true,
|
||||||
|
isDeleted: (json['is_deleted'] as bool?) ?? false,
|
||||||
|
note: json['note'] as String?,
|
||||||
|
createdAt: _parseDate(json['created_at']),
|
||||||
|
updatedAt: _parseDate(json['updated_at']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
'vendor_code': vendorCode,
|
||||||
|
'vendor_name': vendorName,
|
||||||
|
'is_active': isActive,
|
||||||
|
'is_deleted': isDeleted,
|
||||||
|
'note': note,
|
||||||
|
'created_at': createdAt?.toIso8601String(),
|
||||||
|
'updated_at': updatedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Vendor toEntity() => Vendor(
|
||||||
|
id: id,
|
||||||
|
vendorCode: vendorCode,
|
||||||
|
vendorName: vendorName,
|
||||||
|
isActive: isActive,
|
||||||
|
isDeleted: isDeleted,
|
||||||
|
note: note,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
static VendorDto fromEntity(Vendor entity) => VendorDto(
|
||||||
|
id: entity.id,
|
||||||
|
vendorCode: entity.vendorCode,
|
||||||
|
vendorName: entity.vendorName,
|
||||||
|
isActive: entity.isActive,
|
||||||
|
isDeleted: entity.isDeleted,
|
||||||
|
note: entity.note,
|
||||||
|
createdAt: entity.createdAt,
|
||||||
|
updatedAt: entity.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? _parseDate(Object? value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is DateTime) return value;
|
||||||
|
if (value is String) return DateTime.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
70
lib/features/masters/vendor/data/repositories/vendor_repository_remote.dart
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/vendor.dart';
|
||||||
|
import '../../domain/repositories/vendor_repository.dart';
|
||||||
|
import '../dtos/vendor_dto.dart';
|
||||||
|
import '../../../../../core/network/api_client.dart';
|
||||||
|
|
||||||
|
/// 원격 구현체: 공통 ApiClient(Dio) 사용
|
||||||
|
class VendorRepositoryRemote implements VendorRepository {
|
||||||
|
VendorRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
|
||||||
|
|
||||||
|
final ApiClient _api;
|
||||||
|
|
||||||
|
static const _basePath = '/vendors'; // TODO: 백엔드 경로 확정 시 수정
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Vendor>> list({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
String? query,
|
||||||
|
bool includeInactive = true,
|
||||||
|
}) async {
|
||||||
|
final response = await _api.get<List<dynamic>>(
|
||||||
|
_basePath,
|
||||||
|
query: {
|
||||||
|
'page': page,
|
||||||
|
'page_size': pageSize,
|
||||||
|
if (query != null && query.isNotEmpty) 'q': query,
|
||||||
|
if (includeInactive) 'include': 'inactive',
|
||||||
|
},
|
||||||
|
options: Options(responseType: ResponseType.json),
|
||||||
|
);
|
||||||
|
final data = response.data ?? [];
|
||||||
|
return data
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map((e) => VendorDto.fromJson(e).toEntity())
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Vendor> create(Vendor vendor) async {
|
||||||
|
final dto = VendorDto.fromEntity(vendor);
|
||||||
|
final response = await _api.post<Map<String, dynamic>>(
|
||||||
|
_basePath,
|
||||||
|
data: dto.toJson(),
|
||||||
|
options: Options(responseType: ResponseType.json),
|
||||||
|
);
|
||||||
|
return VendorDto.fromJson(response.data ?? {}).toEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Vendor> update(Vendor vendor) async {
|
||||||
|
if (vendor.id == null) {
|
||||||
|
throw ArgumentError('id가 없는 엔티티는 수정할 수 없습니다.');
|
||||||
|
}
|
||||||
|
final dto = VendorDto.fromEntity(vendor);
|
||||||
|
final response = await _api.patch<Map<String, dynamic>>(
|
||||||
|
'$_basePath/${vendor.id}',
|
||||||
|
data: dto.toJson(),
|
||||||
|
options: Options(responseType: ResponseType.json),
|
||||||
|
);
|
||||||
|
return VendorDto.fromJson(response.data ?? {}).toEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(int id) async {
|
||||||
|
await _api.delete<void>('$_basePath/$id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
61
lib/features/masters/vendor/domain/entities/vendor.dart
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/// 벤더(제조사) 도메인 엔티티
|
||||||
|
///
|
||||||
|
/// - SRP: 벤더의 속성 표현만 담당
|
||||||
|
/// - data/presentation 레이어에 의존하지 않음
|
||||||
|
class Vendor {
|
||||||
|
Vendor({
|
||||||
|
this.id,
|
||||||
|
required this.vendorCode,
|
||||||
|
required this.vendorName,
|
||||||
|
this.isActive = true,
|
||||||
|
this.isDeleted = false,
|
||||||
|
this.note,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// PK (DB bigint), 신규 생성 시 null
|
||||||
|
final int? id;
|
||||||
|
|
||||||
|
/// 벤더코드 (부분유니크: is_deleted=false)
|
||||||
|
final String vendorCode;
|
||||||
|
|
||||||
|
/// 벤더명
|
||||||
|
final String vendorName;
|
||||||
|
|
||||||
|
/// 사용 여부
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
|
/// 소프트 삭제 여부
|
||||||
|
final bool isDeleted;
|
||||||
|
|
||||||
|
/// 비고
|
||||||
|
final String? note;
|
||||||
|
|
||||||
|
/// 생성/변경 일시 (선택)
|
||||||
|
final DateTime? createdAt;
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
Vendor copyWith({
|
||||||
|
int? id,
|
||||||
|
String? vendorCode,
|
||||||
|
String? vendorName,
|
||||||
|
bool? isActive,
|
||||||
|
bool? isDeleted,
|
||||||
|
String? note,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
return Vendor(
|
||||||
|
id: id ?? this.id,
|
||||||
|
vendorCode: vendorCode ?? this.vendorCode,
|
||||||
|
vendorName: vendorName ?? this.vendorName,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
isDeleted: isDeleted ?? this.isDeleted,
|
||||||
|
note: note ?? this.note,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
27
lib/features/masters/vendor/domain/repositories/vendor_repository.dart
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import '../entities/vendor.dart';
|
||||||
|
|
||||||
|
/// 벤더 리포지토리 인터페이스
|
||||||
|
///
|
||||||
|
/// - presentation → domain → data 방향을 보장하기 위해 domain에 위치
|
||||||
|
/// - 실제 구현은 data 레이어에서 제공한다.
|
||||||
|
abstract class VendorRepository {
|
||||||
|
/// 벤더 목록 조회
|
||||||
|
///
|
||||||
|
/// - 표준 쿼리 파라미터: page, page_size, q, include
|
||||||
|
Future<List<Vendor>> list({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
String? query,
|
||||||
|
bool includeInactive = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 벤더 생성
|
||||||
|
Future<Vendor> create(Vendor vendor);
|
||||||
|
|
||||||
|
/// 벤더 수정 (부분 업데이트 포함)
|
||||||
|
Future<Vendor> update(Vendor vendor);
|
||||||
|
|
||||||
|
/// 벤더 소프트 삭제
|
||||||
|
Future<void> delete(int id);
|
||||||
|
}
|
||||||
|
|
||||||
175
lib/features/masters/vendor/presentation/pages/vendor_page.dart
vendored
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
|
import '../../../../../core/config/environment.dart';
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
import '../../../vendor/domain/entities/vendor.dart';
|
||||||
|
import '../../../vendor/domain/repositories/vendor_repository.dart';
|
||||||
|
|
||||||
|
class VendorPage extends StatelessWidget {
|
||||||
|
const VendorPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final enabled = Environment.flag('FEATURE_VENDORS_ENABLED');
|
||||||
|
if (!enabled) {
|
||||||
|
return SpecPage(
|
||||||
|
title: '제조사(벤더) 관리',
|
||||||
|
summary: '벤더 기본 정보를 등록하고 사용여부를 제어합니다.',
|
||||||
|
trailing: ShadBadge(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(LucideIcons.info, size: 14),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text('비활성화 (백엔드 준비 중)'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
sections: const [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: ['벤더코드 [Text]', '벤더명 [Text]', '사용여부 [Switch]', '비고 [Text]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '수정 폼',
|
||||||
|
items: ['벤더코드 [ReadOnly]', '생성일시 [ReadOnly]', '수정일시 [ReadOnly]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: ['번호', '벤더코드', '벤더명', '사용여부', '비고', '변경일시'],
|
||||||
|
rows: [
|
||||||
|
['1', 'V-001', '슈퍼벤더', 'Y', '-', '2024-03-01 10:00'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const _VendorEnabledPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VendorEnabledPage extends StatefulWidget {
|
||||||
|
const _VendorEnabledPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_VendorEnabledPage> createState() => _VendorEnabledPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VendorEnabledPageState extends State<_VendorEnabledPage> {
|
||||||
|
final _repo = GetIt.I<VendorRepository>();
|
||||||
|
final _loading = ValueNotifier(false);
|
||||||
|
final _vendors = ValueNotifier<List<Vendor>>([]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_loading.dispose();
|
||||||
|
_vendors.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
_loading.value = true;
|
||||||
|
try {
|
||||||
|
final list = await _repo.list(page: 1, pageSize: 50);
|
||||||
|
_vendors.value = list;
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('벤더 조회 실패: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = ShadTheme.of(context);
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('제조사(벤더) 관리', style: theme.textTheme.h2),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text('벤더코드, 명칭, 사용여부 관리', style: theme.textTheme.muted),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: _loading,
|
||||||
|
builder: (_, loading, __) {
|
||||||
|
return ShadButton(
|
||||||
|
onPressed: loading ? null : _load,
|
||||||
|
child: Text(loading ? '로딩 중...' : '데이터 조회'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ShadCard(
|
||||||
|
title: Text('벤더 목록', style: theme.textTheme.h3),
|
||||||
|
child: ValueListenableBuilder<List<Vendor>>(
|
||||||
|
valueListenable: _vendors,
|
||||||
|
builder: (_, vendors, __) {
|
||||||
|
if (vendors.isEmpty) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Text('데이터가 없습니다. 상단의 "데이터 조회"를 눌러주세요.',
|
||||||
|
style: theme.textTheme.muted),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
height: 56.0 * (vendors.length + 1),
|
||||||
|
child: ShadTable.list(
|
||||||
|
header: const [
|
||||||
|
'ID',
|
||||||
|
'벤더코드',
|
||||||
|
'벤더명',
|
||||||
|
'사용',
|
||||||
|
'비고',
|
||||||
|
'변경일시',
|
||||||
|
].map((h) => ShadTableCell.header(child: Text(h))).toList(),
|
||||||
|
children: vendors
|
||||||
|
.map(
|
||||||
|
(v) => [
|
||||||
|
'${v.id ?? '-'}',
|
||||||
|
v.vendorCode,
|
||||||
|
v.vendorName,
|
||||||
|
v.isActive ? 'Y' : 'N',
|
||||||
|
v.note ?? '-',
|
||||||
|
v.updatedAt?.toIso8601String() ?? '-',
|
||||||
|
].map((c) => ShadTableCell(child: Text(c))).toList(),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
columnSpanExtent: (index) => const FixedTableSpanExtent(160),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class WarehousePage extends StatelessWidget {
|
||||||
|
const WarehousePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '입고지(창고) 관리',
|
||||||
|
summary: '창고 주소와 사용 여부를 구성합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '입력 폼',
|
||||||
|
items: [
|
||||||
|
'창고코드 [Text]',
|
||||||
|
'창고명 [Text]',
|
||||||
|
'우편번호 [검색 연동]',
|
||||||
|
'상세주소 [Text]',
|
||||||
|
'사용여부 [Switch]',
|
||||||
|
'비고 [Text]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '수정 폼',
|
||||||
|
items: ['창고코드 [ReadOnly]', '생성일시 [ReadOnly]'],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '테이블 리스트',
|
||||||
|
description: '1행 예시',
|
||||||
|
table: SpecTable(
|
||||||
|
columns: [
|
||||||
|
'번호',
|
||||||
|
'창고코드',
|
||||||
|
'창고명',
|
||||||
|
'우편번호',
|
||||||
|
'상세주소',
|
||||||
|
'사용여부',
|
||||||
|
'비고',
|
||||||
|
'변경일시',
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'WH-01',
|
||||||
|
'서울 1창고',
|
||||||
|
'04532',
|
||||||
|
'서울시 중구 을지로 100',
|
||||||
|
'Y',
|
||||||
|
'-',
|
||||||
|
'2024-03-01 10:00',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class ReportingPage extends StatelessWidget {
|
||||||
|
const ReportingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '보고서',
|
||||||
|
summary: '기간, 유형, 창고, 상태 조건으로 보고서를 조회하고 내보냅니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '조건 입력',
|
||||||
|
items: [
|
||||||
|
'기간 [Date Range]',
|
||||||
|
'유형 [Dropdown]',
|
||||||
|
'창고 [Dropdown]',
|
||||||
|
'상태 [Dropdown]',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpecSection(
|
||||||
|
title: '출력 옵션',
|
||||||
|
items: ['XLSX 다운로드 [Button]', 'PDF 다운로드 [Button]'],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../../widgets/spec_page.dart';
|
||||||
|
|
||||||
|
class PostalSearchPage extends StatelessWidget {
|
||||||
|
const PostalSearchPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SpecPage(
|
||||||
|
title: '우편번호 검색',
|
||||||
|
summary: '모달 기반 우편번호 검색 UI 구성을 정의합니다.',
|
||||||
|
sections: [
|
||||||
|
SpecSection(
|
||||||
|
title: '모달 구성',
|
||||||
|
items: [
|
||||||
|
'검색어 [Text] 입력 필드',
|
||||||
|
'결과 리스트: 우편번호 | 시도 | 시군구 | 도로명 | 건물번호',
|
||||||
|
'선택 시 호출 화면에 우편번호/주소 전달',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||