summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/common/discipline/README.md96
-rw-r--r--components/common/selectors/nation/README.md183
-rw-r--r--components/common/selectors/purchase-group-code/README.md274
-rw-r--r--components/common/ship-type/README.md123
-rw-r--r--components/common/vendor/README.md211
5 files changed, 887 insertions, 0 deletions
diff --git a/components/common/discipline/README.md b/components/common/discipline/README.md
new file mode 100644
index 00000000..7e95dd4e
--- /dev/null
+++ b/components/common/discipline/README.md
@@ -0,0 +1,96 @@
+# 설계공종코드 선택기 (Engineering Discipline Selector)
+
+내부 PostgreSQL DB에서 설계공종코드(CD_CLF='PLJP43')를 조회하는 공용 컴포넌트입니다.
+
+## 기능
+
+- 내부 PostgreSQL DB에서 설계공종코드 실시간 검색
+- 코드와 이름으로 필터링 가능
+- 페이지네이션 지원
+- 반응형 다이얼로그 UI
+- 서버 액션을 활용한 최적화된 성능
+
+## 사용법
+
+### 기본 사용법
+
+```tsx
+import { EngineeringDisciplineSelector, DisciplineCode } from '@/components/common/discipline'
+
+function MyComponent() {
+ const [selectedDiscipline, setSelectedDiscipline] = useState<DisciplineCode>()
+
+ return (
+ <EngineeringDisciplineSelector
+ selectedDiscipline={selectedDiscipline}
+ onDisciplineSelect={(discipline) => {
+ console.log('선택된 설계공종:', discipline)
+ setSelectedDiscipline(discipline)
+ }}
+ />
+ )
+}
+```
+
+### 커스터마이징
+
+```tsx
+<EngineeringDisciplineSelector
+ selectedDiscipline={selectedDiscipline}
+ onDisciplineSelect={handleSelect}
+ placeholder="설계공종을 선택해주세요"
+ className="w-full h-10"
+ disabled={false}
+ searchOptions={{ limit: 50 }}
+/>
+```
+
+## Props
+
+| Prop | 타입 | 필수 | 기본값 | 설명 |
+|------|------|------|--------|------|
+| `selectedDiscipline` | `DisciplineCode` | ❌ | - | 선택된 설계공종 |
+| `onDisciplineSelect` | `(discipline: DisciplineCode) => void` | ✅ | - | 설계공종 선택 시 호출되는 콜백 |
+| `disabled` | `boolean` | ❌ | `false` | 비활성화 여부 |
+| `placeholder` | `string` | ❌ | `"설계공종을 선택하세요"` | 플레이스홀더 텍스트 |
+| `className` | `string` | ❌ | - | 추가 CSS 클래스 |
+| `searchOptions` | `Partial<DisciplineSearchOptions>` | ❌ | `{ limit: 100 }` | 검색 옵션 |
+
+## 타입
+
+### DisciplineCode
+```tsx
+interface DisciplineCode {
+ CD: string // 설계공종코드
+ USR_DF_CHAR_18: string // 설계공종명
+}
+```
+
+### DisciplineSearchOptions
+```tsx
+interface DisciplineSearchOptions {
+ searchTerm?: string // 검색어
+ limit?: number // 조회 제한 수
+}
+```
+
+## 데이터 소스
+
+**내부 PostgreSQL DB**
+- **테이블**: cmctbCd (NONSAP 스키마)
+- **조건**: CD_CLF = 'PLJP43'
+- **필드**:
+ - CD (설계공종코드)
+ - USR_DF_CHAR_18 (설계공종명)
+
+## 동작 방식
+
+1. **서버 액션 호출**: Next.js 서버 액션을 통한 안전한 데이터 페칭
+2. **PostgreSQL 조회**: Drizzle ORM을 사용하여 cmctbCd 테이블에서 조회
+3. **검색 최적화**: ILIKE를 사용한 대소문자 무관 검색
+4. **성능 최적화**: useTransition을 활용한 논블로킹 UI 업데이트
+
+## 주의사항
+
+- 내부 PostgreSQL DB 연결이 필요합니다
+- NONSAP 스키마의 cmctbCd 테이블에 데이터가 있어야 합니다
diff --git a/components/common/selectors/nation/README.md b/components/common/selectors/nation/README.md
new file mode 100644
index 00000000..f16c3a2f
--- /dev/null
+++ b/components/common/selectors/nation/README.md
@@ -0,0 +1,183 @@
+# 국가 선택기 (Nation Selector)
+
+국가 코드를 선택할 수 있는 컴포넌트들입니다. CMCTB_CD 테이블에서 CD_CLF='LE0010' 조건으로 국가 정보를 조회합니다.
+
+## 컴포넌트 구조
+
+- `nation-service.ts`: 국가 데이터 조회 서버 액션
+- `nation-selector.tsx`: 기본 국가 선택기 (트리거 버튼 포함)
+- `nation-single-selector.tsx`: 단일 선택 다이얼로그
+- `nation-multi-selector.tsx`: 다중 선택 다이얼로그
+
+## 데이터 구조
+
+```typescript
+interface NationCode {
+ CD: string // 2글자 국가코드 (예: KR)
+ CD2: string // 3글자 국가코드 (예: KOR)
+ CD3: string // 3글자 숫자 국가코드 (예: 410)
+ CDNM: string // 한국어 국가명 (예: 대한민국)
+ GRP_DSC: string // 영문 국가명 (예: Korea, Republic of)
+}
+```
+
+## 사용 예제
+
+### 1. 기본 국가 선택기
+
+```tsx
+import { useState } from 'react'
+import { NationSelector, NationCode } from '@/components/common/selectors/nation'
+
+function MyComponent() {
+ const [selectedNation, setSelectedNation] = useState<NationCode>()
+
+ return (
+ <NationSelector
+ selectedNation={selectedNation}
+ onNationSelect={setSelectedNation}
+ placeholder="국가를 선택하세요"
+ />
+ )
+}
+```
+
+### 2. 단일 선택 다이얼로그
+
+```tsx
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import { NationSingleSelector, NationCode } from '@/components/common/selectors/nation'
+
+function MyComponent() {
+ const [open, setOpen] = useState(false)
+ const [selectedNation, setSelectedNation] = useState<NationCode>()
+
+ return (
+ <>
+ <Button onClick={() => setOpen(true)}>
+ 국가 선택
+ </Button>
+
+ <NationSingleSelector
+ open={open}
+ onOpenChange={setOpen}
+ selectedNation={selectedNation}
+ onNationSelect={setSelectedNation}
+ title="국가 선택"
+ description="하나의 국가를 선택하세요"
+ showConfirmButtons={true}
+ onConfirm={(nation) => {
+ console.log('선택된 국가:', nation)
+ }}
+ onCancel={() => {
+ console.log('선택 취소됨')
+ }}
+ />
+ </>
+ )
+}
+```
+
+### 3. 다중 선택 다이얼로그
+
+```tsx
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import { NationMultiSelector, NationCode } from '@/components/common/selectors/nation'
+
+function MyComponent() {
+ const [open, setOpen] = useState(false)
+ const [selectedNations, setSelectedNations] = useState<NationCode[]>([])
+
+ return (
+ <>
+ <Button onClick={() => setOpen(true)}>
+ 국가 다중 선택 ({selectedNations.length}개 선택됨)
+ </Button>
+
+ <NationMultiSelector
+ open={open}
+ onOpenChange={setOpen}
+ selectedNations={selectedNations}
+ onNationsSelect={setSelectedNations}
+ title="국가 다중 선택"
+ description="여러 국가를 선택하세요"
+ maxSelection={5} // 최대 5개까지 선택 가능
+ onConfirm={(nations) => {
+ console.log('선택된 국가들:', nations)
+ }}
+ onCancel={() => {
+ console.log('선택 취소됨')
+ }}
+ />
+ </>
+ )
+}
+```
+
+### 4. 서버 액션 직접 사용
+
+```tsx
+import { getNationCodes, getNationCodeByCode } from '@/components/common/selectors/nation'
+
+// 모든 국가 조회
+const result = await getNationCodes()
+if (result.success) {
+ console.log('국가 목록:', result.data)
+}
+
+// 검색 조건으로 조회
+const searchResult = await getNationCodes({
+ searchTerm: '한국',
+ limit: 50
+})
+
+// 특정 국가 코드로 조회
+const korea = await getNationCodeByCode('KR')
+console.log('대한민국:', korea)
+```
+
+## Props 옵션
+
+### NationSelector
+
+- `selectedNation?: NationCode` - 선택된 국가
+- `onNationSelect: (nation: NationCode) => void` - 국가 선택 콜백
+- `disabled?: boolean` - 비활성화 여부
+- `placeholder?: string` - 플레이스홀더 텍스트
+- `className?: string` - 추가 CSS 클래스
+- `searchOptions?: Partial<NationSearchOptions>` - 검색 옵션
+
+### NationSingleSelector
+
+- `open: boolean` - 다이얼로그 열림 상태
+- `onOpenChange: (open: boolean) => void` - 다이얼로그 상태 변경 콜백
+- `selectedNation?: NationCode` - 선택된 국가
+- `onNationSelect: (nation: NationCode) => void` - 국가 선택 콜백
+- `onConfirm?: (nation: NationCode | undefined) => void` - 확인 버튼 콜백
+- `onCancel?: () => void` - 취소 버튼 콜백
+- `title?: string` - 다이얼로그 제목
+- `description?: string` - 다이얼로그 설명
+- `showConfirmButtons?: boolean` - 확인/취소 버튼 표시 여부
+
+### NationMultiSelector
+
+- `open: boolean` - 다이얼로그 열림 상태
+- `onOpenChange: (open: boolean) => void` - 다이얼로그 상태 변경 콜백
+- `selectedNations?: NationCode[]` - 선택된 국가들
+- `onNationsSelect: (nations: NationCode[]) => void` - 국가들 선택 콜백
+- `onConfirm?: (nations: NationCode[]) => void` - 확인 버튼 콜백
+- `onCancel?: () => void` - 취소 버튼 콜백
+- `title?: string` - 다이얼로그 제목
+- `description?: string` - 다이얼로그 설명
+- `maxSelection?: number` - 최대 선택 가능 개수
+
+## 특징
+
+- **검색 기능**: 국가코드, 국가명으로 실시간 검색
+- **페이지네이션**: 대량의 데이터를 페이지별로 표시
+- **디바운스**: 검색 성능 최적화
+- **다국어 지원**: 한국어명과 영문명 모두 표시
+- **접근성**: 키보드 네비게이션 및 스크린 리더 지원
+- **반응형**: 다양한 화면 크기에 대응
diff --git a/components/common/selectors/purchase-group-code/README.md b/components/common/selectors/purchase-group-code/README.md
new file mode 100644
index 00000000..b956a296
--- /dev/null
+++ b/components/common/selectors/purchase-group-code/README.md
@@ -0,0 +1,274 @@
+# 구매그룹코드 선택기 (Purchase Group Code Selector)
+
+구매그룹코드를 선택할 수 있는 컴포넌트들입니다. Oracle DB의 CMCTB_CDNM, CMCTB_CD 테이블에서 CD_CLF='MMA070' 조건으로 구매그룹 정보를 조회하며, 선택 시 해당 사번의 사용자 정보도 함께 반환합니다.
+
+## 컴포넌트 구조
+
+- `purchase-group-code-service.ts`: 구매그룹코드 데이터 조회 서버 액션 (Oracle DB 조회 + 사용자 정보 포함)
+- `purchase-group-code-selector.tsx`: 기본 구매그룹코드 선택기 (트리거 버튼 포함)
+- `purchase-group-code-single-selector.tsx`: 단일 선택 다이얼로그
+- `purchase-group-code-multi-selector.tsx`: 다중 선택 다이얼로그
+
+## 데이터 구조
+
+```typescript
+// 기본 구매그룹코드 정보
+interface PurchaseGroupCode {
+ PURCHASE_GROUP_CODE: string // 구매그룹코드
+ DISPLAY_NAME: string // 표시명 (이름_부서_퇴직/전배정보)
+ EMPLOYEE_NUMBER: string // 사번
+}
+
+// 사용자 정보를 포함한 구매그룹코드
+interface PurchaseGroupCodeWithUser extends PurchaseGroupCode {
+ user?: {
+ id: number
+ name: string
+ email: string
+ employeeNumber: string | null
+ userCode: string | null
+ } | null
+}
+```
+
+## 사용 예제
+
+### 1. 기본 구매그룹코드 선택기
+
+```tsx
+import { useState } from 'react'
+import { PurchaseGroupCodeSelector, PurchaseGroupCodeWithUser } from '@/components/common/selectors/purchase-group-code'
+
+function MyComponent() {
+ const [selectedCode, setSelectedCode] = useState<PurchaseGroupCodeWithUser>()
+
+ const handleSelect = (code: PurchaseGroupCodeWithUser) => {
+ console.log('선택된 구매그룹코드:', code.PURCHASE_GROUP_CODE)
+ console.log('표시명:', code.DISPLAY_NAME)
+ console.log('사번:', code.EMPLOYEE_NUMBER)
+ console.log('사용자 정보:', code.user)
+ setSelectedCode(code)
+ }
+
+ return (
+ <PurchaseGroupCodeSelector
+ selectedCode={selectedCode}
+ onCodeSelect={handleSelect}
+ placeholder="구매그룹코드를 선택하세요"
+ />
+ )
+}
+```
+
+### 2. 단일 선택 다이얼로그
+
+```tsx
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import {
+ PurchaseGroupCodeSingleSelector,
+ PurchaseGroupCodeWithUser
+} from '@/components/common/selectors/purchase-group-code'
+
+function MyComponent() {
+ const [open, setOpen] = useState(false)
+ const [selectedCode, setSelectedCode] = useState<PurchaseGroupCodeWithUser>()
+
+ return (
+ <>
+ <Button onClick={() => setOpen(true)}>
+ 구매그룹코드 선택
+ </Button>
+
+ <PurchaseGroupCodeSingleSelector
+ open={open}
+ onOpenChange={setOpen}
+ selectedCode={selectedCode}
+ onCodeSelect={setSelectedCode}
+ title="구매그룹코드 선택"
+ description="하나의 구매그룹코드를 선택하세요"
+ showConfirmButtons={true}
+ onConfirm={(code) => {
+ console.log('선택 완료:', code)
+ if (code?.user) {
+ console.log('연결된 사용자:', code.user.name)
+ }
+ }}
+ onCancel={() => {
+ console.log('선택 취소됨')
+ }}
+ />
+ </>
+ )
+}
+```
+
+### 3. 다중 선택 다이얼로그
+
+```tsx
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import {
+ PurchaseGroupCodeMultiSelector,
+ PurchaseGroupCodeWithUser
+} from '@/components/common/selectors/purchase-group-code'
+
+function MyComponent() {
+ const [open, setOpen] = useState(false)
+ const [selectedCodes, setSelectedCodes] = useState<PurchaseGroupCodeWithUser[]>([])
+
+ return (
+ <>
+ <Button onClick={() => setOpen(true)}>
+ 구매그룹코드 다중 선택 ({selectedCodes.length}개 선택됨)
+ </Button>
+
+ <PurchaseGroupCodeMultiSelector
+ open={open}
+ onOpenChange={setOpen}
+ selectedCodes={selectedCodes}
+ onCodesSelect={setSelectedCodes}
+ title="구매그룹코드 다중 선택"
+ description="여러 구매그룹코드를 선택하세요"
+ maxSelection={5} // 최대 5개까지 선택 가능
+ onConfirm={(codes) => {
+ console.log('선택된 구매그룹코드들:', codes)
+ codes.forEach(code => {
+ console.log(`${code.PURCHASE_GROUP_CODE}: ${code.user?.name}`)
+ })
+ }}
+ onCancel={() => {
+ console.log('선택 취소됨')
+ }}
+ />
+ </>
+ )
+}
+```
+
+### 4. 서버 액션 직접 사용
+
+```tsx
+import {
+ getPurchaseGroupCodes,
+ getPurchaseGroupCodeWithUser,
+ getPurchaseGroupCodeByEmployeeNumber
+} from '@/components/common/selectors/purchase-group-code'
+
+// 모든 구매그룹코드 조회
+const result = await getPurchaseGroupCodes()
+if (result.success) {
+ console.log('구매그룹코드 목록:', result.data)
+}
+
+// 검색 조건으로 조회
+const searchResult = await getPurchaseGroupCodes({
+ searchTerm: '홍길동',
+ limit: 50
+})
+
+// 특정 구매그룹코드로 조회 (사용자 정보 포함)
+const codeWithUser = await getPurchaseGroupCodeWithUser('PG001')
+if (codeWithUser) {
+ console.log('구매그룹코드:', codeWithUser.PURCHASE_GROUP_CODE)
+ console.log('사용자:', codeWithUser.user?.name)
+}
+
+// 사번으로 구매그룹코드 조회 (사용자 정보 포함)
+const byEmployeeNumber = await getPurchaseGroupCodeByEmployeeNumber('1234567')
+if (byEmployeeNumber) {
+ console.log('해당 사번의 구매그룹코드:', byEmployeeNumber.PURCHASE_GROUP_CODE)
+}
+```
+
+## Props 옵션
+
+### PurchaseGroupCodeSelector
+
+- `selectedCode?: PurchaseGroupCodeWithUser` - 선택된 구매그룹코드
+- `onCodeSelect: (code: PurchaseGroupCodeWithUser) => void` - 구매그룹코드 선택 콜백
+- `disabled?: boolean` - 비활성화 여부
+- `placeholder?: string` - 플레이스홀더 텍스트
+- `className?: string` - 추가 CSS 클래스
+- `searchOptions?: Partial<PurchaseGroupCodeSearchOptions>` - 검색 옵션
+
+### PurchaseGroupCodeSingleSelector
+
+- `open: boolean` - 다이얼로그 열림 상태
+- `onOpenChange: (open: boolean) => void` - 다이얼로그 상태 변경 콜백
+- `selectedCode?: PurchaseGroupCodeWithUser` - 선택된 구매그룹코드
+- `onCodeSelect: (code: PurchaseGroupCodeWithUser) => void` - 구매그룹코드 선택 콜백
+- `onConfirm?: (code: PurchaseGroupCodeWithUser | undefined) => void` - 확인 버튼 콜백
+- `onCancel?: () => void` - 취소 버튼 콜백
+- `title?: string` - 다이얼로그 제목
+- `description?: string` - 다이얼로그 설명
+- `showConfirmButtons?: boolean` - 확인/취소 버튼 표시 여부
+
+### PurchaseGroupCodeMultiSelector
+
+- `open: boolean` - 다이얼로그 열림 상태
+- `onOpenChange: (open: boolean) => void` - 다이얼로그 상태 변경 콜백
+- `selectedCodes?: PurchaseGroupCodeWithUser[]` - 선택된 구매그룹코드들
+- `onCodesSelect: (codes: PurchaseGroupCodeWithUser[]) => void` - 구매그룹코드들 선택 콜백
+- `onConfirm?: (codes: PurchaseGroupCodeWithUser[]) => void` - 확인 버튼 콜백
+- `onCancel?: () => void` - 취소 버튼 콜백
+- `title?: string` - 다이얼로그 제목
+- `description?: string` - 다이얼로그 설명
+- `maxSelection?: number` - 최대 선택 가능 개수
+
+## 특징
+
+- **Oracle DB 연동**: Oracle NonSAP 데이터베이스에서 실시간 조회
+- **폴백 테스트 데이터**: Oracle DB 연결 실패 시 자동으로 테스트 데이터 사용
+- **사용자 정보 통합**: 선택 시 사번으로 연결된 사용자 정보 자동 조회
+- **검색 기능**: 구매그룹코드, 이름, 사번으로 실시간 검색
+- **페이지네이션**: 대량의 데이터를 페이지별로 표시
+- **디바운스**: 검색 성능 최적화
+- **다중 선택 지원**: 여러 구매그룹코드 동시 선택 가능
+- **접근성**: 키보드 네비게이션 및 스크린 리더 지원
+- **반응형**: 다양한 화면 크기에 대응
+
+## 데이터 소스
+
+구매그룹코드는 Oracle DB의 다음 테이블에서 조회됩니다:
+
+```sql
+SELECT CD.CD AS PURCHASE_GROUP_CODE,
+ NM.CDNM AS DISPLAY_NAME,
+ CD.USR_DF_CHAR_9 AS EMPLOYEE_NUMBER
+FROM CMCTB_CDNM NM
+JOIN CMCTB_CD CD ON NM.CD_CLF = CD.CD_CLF
+ AND NM.CD = CD.CD
+ AND NM.CD2 = CD.CD3
+WHERE NM.CD_CLF = 'MMA070'
+ AND CD.USR_DF_CHAR_9 IS NOT NULL
+```
+
+사용자 정보는 PostgreSQL DB의 `users` 테이블에서 `employeeNumber`로 조회됩니다.
+
+## 주의사항
+
+1. **Oracle DB 연결 필요**: 이 컴포넌트는 Oracle DB 연결이 필요합니다. 환경 변수가 올바르게 설정되어 있는지 확인하세요.
+2. **폴백 테스트 데이터**: Oracle DB 연결 실패 시 하드코딩된 10개의 테스트 데이터가 자동으로 사용됩니다. 테스트 환경에서 유용합니다.
+3. **사용자 정보 매칭**: 사번으로 사용자 정보를 조회하므로, 사용자 테이블에 해당 사번이 존재해야 합니다.
+4. **성능**: 검색 시 Oracle DB를 직접 조회하므로, 대량 데이터 환경에서는 인덱스 설정이 중요합니다.
+
+## 폴백 테스트 데이터
+
+Oracle DB 연결이 불가능한 테스트 환경에서는 다음 10개의 구매그룹코드가 자동으로 제공됩니다:
+
+- `12L` - 김철수_구매팀_재직
+- `32F` - 이영희_자재팀_재직
+- `45A` - 박민수_조달팀_재직
+- `67K` - 정수진_구매1팀_재직
+- `89D` - 최동욱_구매2팀_재직
+- `11B` - 강미라_자재관리팀_재직
+- `23G` - 윤성호_구매기획팀_재직
+- `56H` - 임지훈_조달지원팀_재직
+- `78M` - 한소희_구매운영팀_재직
+- `90C` - 오준석_전략구매팀_재직
+
+## 관련 서비스
+
+- 구매그룹코드 동기화: `lib/nonsap-sync/purchase-group-code/purchase-group-code-sync.ts`
+- 사용자 서비스: `lib/users/service.ts`
diff --git a/components/common/ship-type/README.md b/components/common/ship-type/README.md
new file mode 100644
index 00000000..4b50773d
--- /dev/null
+++ b/components/common/ship-type/README.md
@@ -0,0 +1,123 @@
+# 선종 선택기 (Ship Type Selector)
+
+선종 정보를 검색하고 선택할 수 있는 컴포넌트입니다.
+
+## 컴포넌트 구조
+
+```text
+components/common/ship-type/
+├── ship-type-service.ts # 서버 액션 (선종 데이터 조회)
+├── ship-type-selector.tsx # 선종 선택기 컴포넌트
+├── index.ts # 모듈 export
+└── README.md # 문서
+```
+
+## 데이터 소스
+
+**내부 PostgreSQL DB - `cmctb_cdnm` 테이블**
+
+- 조건: `CD_CLF = 'PSA330' AND DEL_YN = 'N'`
+- 선종코드: `CD` 컬럼
+- 선종명: `CDNM` 컬럼
+
+## 기본 사용법
+
+```tsx
+import { ShipTypeSelector, ShipTypeItem } from '@/components/common/ship-type'
+
+function MyComponent() {
+ const [selectedShipType, setSelectedShipType] = useState<ShipTypeItem | undefined>()
+
+ const handleShipTypeSelect = (shipType: ShipTypeItem) => {
+ setSelectedShipType(shipType)
+ console.log('선택된 선종:', shipType.CD, shipType.CDNM)
+ }
+
+ return (
+ <ShipTypeSelector
+ selectedShipType={selectedShipType}
+ onShipTypeSelect={handleShipTypeSelect}
+ placeholder="선종을 선택하세요"
+ />
+ )
+}
+```
+
+## Props
+
+### ShipTypeSelector
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `selectedShipType` | `ShipTypeItem \| undefined` | - | 현재 선택된 선종 |
+| `onShipTypeSelect` | `(shipType: ShipTypeItem) => void` | - | 선종 선택 시 호출되는 콜백 함수 |
+| `disabled` | `boolean` | `false` | 선택기 비활성화 여부 |
+| `placeholder` | `string` | `"선종을 선택하세요"` | 선택되지 않았을 때 표시할 텍스트 |
+| `className` | `string` | - | 추가 CSS 클래스 |
+
+## 타입 정의
+
+### ShipTypeItem
+
+```tsx
+interface ShipTypeItem {
+ CD: string // 선종코드
+ CDNM: string // 선종명
+ displayText: string // 표시용 텍스트 (CD + " - " + CDNM)
+}
+```
+
+### ShipTypeSearchOptions
+
+```tsx
+interface ShipTypeSearchOptions {
+ searchTerm?: string // 검색어 (선종코드 또는 선종명)
+ limit?: number // 조회 결과 제한 (기본값: 100)
+}
+```
+
+## 서버 액션
+
+### getShipTypes(options)
+
+선종 목록을 조회합니다.
+
+```tsx
+const result = await getShipTypes({
+ searchTerm: "CONT",
+ limit: 50
+})
+
+if (result.success) {
+ console.log('선종 목록:', result.data)
+} else {
+ console.error('오류:', result.error)
+}
+```
+
+### getShipTypeByCode(code)
+
+특정 선종코드로 선종 정보를 조회합니다.
+
+```tsx
+const shipType = await getShipTypeByCode("CONT")
+if (shipType) {
+ console.log('선종 정보:', shipType.CD, shipType.CDNM)
+}
+```
+
+## 특징
+
+- ✅ **즉시 검색**: 검색어 입력 시 실시간으로 결과 필터링
+- ✅ **디바운싱**: 300ms 디바운스로 API 호출 최적화
+- ✅ **서버 액션**: `useTransition`을 사용한 논블로킹 서버 호출
+- ✅ **페이지네이션**: 대량 데이터 지원 (기본 페이지당 10개)
+- ✅ **검색 최적화**: 선종코드와 선종명 모두 검색 가능
+- ✅ **사용자 친화적**: Dialog 기반 선택 UI
+
+## 주의사항
+
+- 선종 데이터는 약 50개 정도로 페이지네이션 없이도 충분히 처리 가능
+- 검색은 선종코드(`CD`)와 선종명(`CDNM`) 모두에서 수행됩니다
+- 대소문자 구분 없이 검색 가능 (ILIKE 사용)
+- `DEL_YN = 'N'` 조건으로 삭제되지 않은 선종만 조회
diff --git a/components/common/vendor/README.md b/components/common/vendor/README.md
new file mode 100644
index 00000000..7c9e54d9
--- /dev/null
+++ b/components/common/vendor/README.md
@@ -0,0 +1,211 @@
+# 벤더 선택기 (Vendor Selector)
+
+벤더 선택을 위한 공용 컴포넌트입니다. 다양한 형태로 벤더를 검색하고 선택할 수 있는 기능을 제공합니다.
+
+## 기능
+
+- PostgreSQL DB에서 벤더 실시간 검색 및 선택
+- 벤더명, 벤더코드로 필터링 가능
+- 상태별 필터링 지원 (ACTIVE, PENDING_REVIEW, APPROVED, INACTIVE 등)
+- 페이지네이션 지원
+- 단일/다중 선택 모드
+- 최대 선택 개수 제한
+- 제외 벤더 설정 가능
+- 반응형 다이얼로그 UI
+
+## 컴포넌트 구조
+
+### 1. VendorSelector (기본 선택기)
+
+Popover 기반의 기본 벤더 선택기 컴포넌트
+
+### 2. VendorSelectorDialogSingle (단일 선택 다이얼로그)
+
+Dialog 형태의 단일 벤더 선택 컴포넌트
+
+### 3. VendorSelectorDialogMulti (다중 선택 다이얼로그)
+
+Dialog 형태의 다중 벤더 선택 컴포넌트
+
+## 사용법
+
+### 기본 선택기 사용법
+
+```tsx
+import { VendorSelector, VendorSearchItem } from '@/components/common/vendor'
+
+function MyComponent() {
+ const [selectedVendors, setSelectedVendors] = useState<VendorSearchItem[]>([])
+
+ return (
+ <VendorSelector
+ selectedVendors={selectedVendors}
+ onVendorsChange={(vendors) => {
+ console.log('선택된 벤더들:', vendors)
+ setSelectedVendors(vendors)
+ }}
+ placeholder="벤더를 검색하세요..."
+ statusFilter="ACTIVE" // ACTIVE 상태의 벤더만 표시
+ />
+ )
+}
+```
+
+### 단일 선택 다이얼로그 사용법
+
+```tsx
+import { VendorSelectorDialogSingle, VendorSearchItem } from '@/components/common/vendor'
+
+function MyComponent() {
+ const [selectedVendor, setSelectedVendor] = useState<VendorSearchItem | null>(null)
+
+ return (
+ <VendorSelectorDialogSingle
+ triggerLabel="벤더 선택"
+ selectedVendor={selectedVendor}
+ onVendorSelect={(vendor) => {
+ console.log('선택된 벤더:', vendor)
+ setSelectedVendor(vendor)
+ }}
+ title="협력업체 선택"
+ description="프로젝트에 참여할 협력업체를 선택해주세요."
+ />
+ )
+}
+```
+
+### 다중 선택 다이얼로그 사용법
+
+```tsx
+import { VendorSelectorDialogMulti, VendorSearchItem } from '@/components/common/vendor'
+
+function MyComponent() {
+ const [selectedVendors, setSelectedVendors] = useState<VendorSearchItem[]>([])
+
+ return (
+ <VendorSelectorDialogMulti
+ triggerLabel="벤더 선택 (다중)"
+ selectedVendors={selectedVendors}
+ onVendorsSelect={(vendors) => {
+ console.log('선택된 벤더들:', vendors)
+ setSelectedVendors(vendors)
+ }}
+ maxSelections={5}
+ title="협력업체 선택"
+ description="프로젝트에 참여할 협력업체들을 선택해주세요. (최대 5개)"
+ showSelectedInTrigger={true}
+ />
+ )
+}
+```
+
+## Props
+
+### VendorSelector Props
+
+| Prop | 타입 | 필수 | 기본값 | 설명 |
+|------|------|------|--------|------|
+| `selectedVendors` | `VendorSearchItem[]` | ❌ | `[]` | 선택된 벤더들 |
+| `onVendorsChange` | `(vendors: VendorSearchItem[]) => void` | ❌ | - | 벤더 선택 변경 시 호출되는 콜백 |
+| `singleSelect` | `boolean` | ❌ | `false` | 단일 선택 모드 여부 |
+| `placeholder` | `string` | ❌ | `"벤더를 검색하세요..."` | 검색 입력창 placeholder |
+| `noValuePlaceHolder` | `string` | ❌ | `"벤더를 검색해주세요"` | 선택된 벤더가 없을 때 표시되는 텍스트 |
+| `disabled` | `boolean` | ❌ | `false` | 비활성화 여부 |
+| `className` | `string` | ❌ | - | 추가 CSS 클래스 |
+| `closeOnSelect` | `boolean` | ❌ | `true` | 선택 후 자동 닫기 여부 |
+| `excludeVendorIds` | `Set<number>` | ❌ | - | 제외할 벤더 ID들 |
+| `showInitialData` | `boolean` | ❌ | `true` | 초기 데이터 표시 여부 |
+| `maxSelections` | `number` | ❌ | - | 최대 선택 가능한 벤더 개수 |
+| `statusFilter` | `string` | ❌ | - | 벤더 상태 필터 |
+
+### VendorSelectorDialogSingle Props
+
+| Prop | 타입 | 필수 | 기본값 | 설명 |
+|------|------|------|--------|------|
+| `triggerLabel` | `string` | ❌ | `"벤더 선택"` | 트리거 버튼 텍스트 |
+| `selectedVendor` | `VendorSearchItem \| null` | ❌ | `null` | 선택된 벤더 |
+| `onVendorSelect` | `(vendor: VendorSearchItem \| null) => void` | ❌ | - | 벤더 선택 완료 시 호출되는 콜백 |
+| `placeholder` | `string` | ❌ | `"벤더를 검색하세요..."` | 검색 입력창 placeholder |
+| `title` | `string` | ❌ | `"벤더 선택"` | Dialog 제목 |
+| `description` | `string` | ❌ | `"원하는 벤더를 검색하고 선택해주세요."` | Dialog 설명 |
+| `disabled` | `boolean` | ❌ | `false` | 트리거 버튼 비활성화 여부 |
+| `triggerVariant` | `ButtonVariant` | ❌ | `"outline"` | 트리거 버튼 variant |
+| `excludeVendorIds` | `Set<number>` | ❌ | - | 제외할 벤더 ID들 |
+| `showInitialData` | `boolean` | ❌ | `true` | 초기 데이터 표시 여부 |
+| `statusFilter` | `string` | ❌ | - | 벤더 상태 필터 |
+
+### VendorSelectorDialogMulti Props
+
+VendorSelectorDialogSingle과 유사하지만 다음이 추가됩니다:
+
+| Prop | 타입 | 필수 | 기본값 | 설명 |
+|------|------|------|--------|------|
+| `selectedVendors` | `VendorSearchItem[]` | ❌ | `[]` | 선택된 벤더들 |
+| `onVendorsSelect` | `(vendors: VendorSearchItem[]) => void` | ❌ | - | 벤더 선택 완료 시 호출되는 콜백 |
+| `maxSelections` | `number` | ❌ | - | 최대 선택 가능한 벤더 개수 |
+| `showSelectedInTrigger` | `boolean` | ❌ | `true` | 트리거 버튼에서 선택된 벤더들을 표시할지 여부 |
+
+## 타입
+
+### VendorSearchItem
+
+```tsx
+interface VendorSearchItem {
+ id: number // 벤더 ID
+ vendorName: string // 벤더명
+ vendorCode: string | null // 벤더코드 (없을 수 있음)
+ status: string // 벤더 상태
+ displayText: string // 표시용 텍스트 (vendorName + vendorCode)
+}
+```
+
+### VendorSearchOptions
+
+```tsx
+interface VendorSearchOptions {
+ searchTerm?: string // 검색어
+ statusFilter?: string // 상태 필터
+ limit?: number // 조회 제한 수
+ offset?: number // 조회 시작 위치
+ sortBy?: 'vendorName' | 'vendorCode' | 'status' // 정렬 기준
+ sortOrder?: 'asc' | 'desc' // 정렬 순서
+}
+```
+
+### VendorPagination
+
+```tsx
+interface VendorPagination {
+ page: number // 현재 페이지
+ perPage: number // 페이지당 항목 수
+ total: number // 전체 항목 수
+ pageCount: number // 전체 페이지 수
+ hasNextPage: boolean // 다음 페이지 존재 여부
+ hasPrevPage: boolean // 이전 페이지 존재 여부
+}
+```
+
+## 데이터 소스
+
+**PostgreSQL DB의 `vendors` 테이블**
+
+- **필드**:
+ - id (벤더 ID)
+ - vendorName (벤더명)
+ - vendorCode (벤더코드)
+ - status (벤더 상태)
+
+## 동작 방식
+
+1. **서버 액션 호출**: Next.js 서버 액션을 통한 안전한 데이터 페칭
+2. **PostgreSQL 조회**: Drizzle ORM을 사용하여 vendors 테이블에서 조회
+3. **검색 최적화**: ILIKE를 사용한 대소문자 무관 검색
+4. **페이지네이션**: 대량 데이터 처리를 위한 페이지네이션 지원
+5. **상태 필터링**: 벤더 상태별 필터링 지원
+
+## 주의사항
+
+- PostgreSQL DB 연결이 필요합니다
+- vendors 스키마의 vendors 테이블에 데이터가 있어야 합니다
+- 검색은 벤더명과 벤더코드에 대해서만 수행됩니다
+- 상태 값은 테이블에 정의된 값을 사용해야 합니다