Browse Source

+ 작업중 파일

송용우 4 months ago
parent
commit
6ceaa8e61c
65 changed files with 8973 additions and 606 deletions
  1. 54 1
      .cursor/rules/api-rule.mdc
  2. 87 0
      .cursor/rules/db-structure-rules.mdc
  3. 123 0
      .cursor/rules/sql-rules.mdc
  4. 62 2
      .vooster/tasks.json
  5. 31 0
      .vooster/tasks/T-016.txt
  6. 30 0
      .vooster/tasks/T-017.txt
  7. 30 0
      .vooster/tasks/T-018.txt
  8. 29 0
      .vooster/tasks/T-019.txt
  9. 29 0
      .vooster/tasks/T-020.txt
  10. 29 0
      .vooster/tasks/T-021.txt
  11. 135 0
      backend/README.md
  12. 186 0
      backend/app/Config/Routes.php
  13. 911 0
      backend/app/Controllers/Auth.php
  14. 156 0
      backend/app/Controllers/DebugController.php
  15. 86 0
      backend/app/Controllers/InfluencerController.php
  16. 198 0
      backend/app/Controllers/Roulette.php
  17. 622 0
      backend/app/Controllers/VendorInfluencerController.php
  18. 223 0
      backend/app/Models/UserModel.php
  19. 147 0
      backend/app/Models/VendorInfluencerMappingModel.php
  20. 189 0
      backend/app/Models/VendorModel.php
  21. 2 5
      components/common/header.vue
  22. 2 3
      components/common/leftMenu.vue
  23. 54 0
      composables/useLogout.js
  24. 47 0
      database/migrations/create_vendor_influencer_mapping.sql
  25. 11 0
      database/url.md
  26. 31 0
      ddl/001_create_vendor_influencer_mapping_table.sql
  27. 39 0
      ddl/002_add_vendor_influencer_mapping_indexes.sql
  28. 27 0
      ddl/003_add_vendor_influencer_mapping_foreign_keys.sql
  29. 35 0
      ddl/004_add_vendor_list_additional_columns.sql
  30. 91 0
      ddl/004_add_vendor_list_additional_columns_safe.sql
  31. 38 0
      ddl/004_add_vendor_list_additional_columns_simple.sql
  32. 34 0
      ddl/004_vendor_check_and_add_columns.sql
  33. 64 0
      ddl/005_add_user_list_additional_columns.sql
  34. 168 0
      ddl/005_add_user_list_additional_columns_safe.sql
  35. 66 0
      ddl/005_add_user_list_additional_columns_simple.sql
  36. 32 0
      ddl/006_create_partnership_history_table.sql
  37. 41 0
      ddl/007_create_notification_table.sql
  38. 74 0
      ddl/008_create_sample_data_inserts.sql
  39. 49 0
      ddl/009_add_indexes_after_columns_exist.sql
  40. 28 0
      ddl/009_add_vendor_list_indexes.sql
  41. 80 0
      ddl/009_add_vendor_list_indexes_safe.sql
  42. 30 0
      ddl/009_add_vendor_list_indexes_simple.sql
  43. 34 0
      ddl/010_add_user_list_indexes.sql
  44. 102 0
      ddl/010_add_user_list_indexes_safe.sql
  45. 36 0
      ddl/010_add_user_list_indexes_simple.sql
  46. 20 0
      ddl/011_fix_vendor_influencer_mapping_columns.sql
  47. 134 0
      ddl/012_add_vendor_list_essential_columns.sql
  48. 25 0
      ddl/013_simple_vendor_list_columns.sql
  49. 82 0
      ddl/014_update_vendor_test_data.sql
  50. 30 0
      ddl/015_quick_vendor_data_update.sql
  51. 51 0
      ddl/016_add_user_list_additional_columns.sql
  52. 87 0
      ddl/017_add_test_influencer_data.sql
  53. 114 0
      ddl/README.md
  54. 110 0
      ddl/README_SAFE.md
  55. 5 0
      ddl/check_current_table_structure.sql
  56. 75 0
      ddl/fix_missing_columns.sql
  57. 2 1
      nuxt.config.ts
  58. 6 2
      pages/index.vue
  59. 624 0
      pages/view/influencer/[id].vue
  60. 350 297
      pages/view/vendor/[id].vue
  61. 1121 0
      pages/view/vendor/dashboard/influencer-requests.vue
  62. 941 0
      pages/view/vendor/search.vue
  63. 592 283
      pages/view/vendor/vendors.vue
  64. 0 3
      server/tsconfig.json
  65. 32 9
      stores/auth.js

+ 54 - 1
.cursor/rules/api-rule.mdc

@@ -5,4 +5,57 @@ alwaysApply: true
 
 - api 서버는 코드이그나이터4 베이스의 벡엔드 기술로 구현되어있으며
   기존 문서에사용되는 양식을 지키며 구현
-- 프론트에서 api신규 생성시 백엔드 코드이그나4 기반의 기술로 구현하는 예제를 함께 제공
+- 프론트에서 api신규 생성시 백엔드 코드이그나4 기반의 기술로 구현하는 예제를 함께 제공
+- 항상 페이지 구성이 완료되고 나면 제작에 필요한 쿼리를 DDL형태로 구성해서 ddl폴더에 만들어줘
+- api구성후 백엔드 예제를 backend-examples에 코드이그나이터4 형태로 구성해줘
+- MD파일을 생성해서 백엔드 구성과 DB생성을 하는 과정을 순서대로 작성해줘
+- 프론트화면 및 UI / API 구성시에는 항상 composition api 형태로 작성 css는 항상 scss형태로 분리해서 구성
+
+## 프론트엔드 API 호출 규칙
+- **Nuxt.js server/api 사용 금지**: 프론트엔드에서 직접 백엔드 API 호출
+- **useAxios() 패턴 강제**: 기존 코드베이스와 일관성 유지
+- 반드시 다음 형태로 구성:
+```javascript
+const loadData = async () => {
+  try {
+    const params = {
+      // 파라미터들...
+    }
+
+    useAxios()
+      .post('/api/endpoint', params)
+      .then((res) => {
+        if (res.data.success) {
+          // 성공 처리
+          data.value = res.data.data
+        } else {
+          // 실패 처리
+          error.value = res.data.message
+        }
+      })
+      .catch((err) => {
+        // 에러 처리
+        error.value = err.message
+      })
+      .finally(() => {
+        // 완료 처리
+        loading.value = false
+      })
+
+  } catch (err) {
+    error.value = err.message
+  }
+}
+```
+
+## API 구조 금지사항
+- **server/api 디렉토리 생성 금지**: Nuxt.js 서버 API 사용하지 않음
+- **mysql2, 데이터베이스 라이브러리 사용 금지**: 프론트엔드에서 직접 DB 연결 금지
+- **$fetch 사용 금지**: useAxios() 패턴만 사용
+- **async/await 패턴 지양**: .then().catch().finally() 체인 사용
+
+## 백엔드 연동 방식
+- 프론트엔드 → CodeIgniter4 백엔드 직접 호출
+- useAxios()를 통한 HTTP 통신만 사용
+- 응답 형태: `res.data.success`, `res.data.data`, `res.data.message`
+- 백엔드는 직접 만들거야 다만 너가 backend-examples 폴더에 프론트와 수신할수는있는 형태의 api예제를 만들어

+ 87 - 0
.cursor/rules/db-structure-rules.mdc

@@ -0,0 +1,87 @@
+# 데이터베이스 구조 규칙
+
+## 1. 사용자 구분 및 관리
+
+### 1.1 인플루언서 (USER_LIST)
+- 인플루언서 정보는 USER_LIST 테이블에서 관리
+- 주요 필드:
+  ```sql
+  SEQ: 고유 식별자
+  NICK_NAME: 닉네임
+  NAME: 실명
+  EMAIL: 이메일
+  PROFILE_IMAGE: 프로필 이미지
+  PRIMARY_CATEGORY: 주요 카테고리
+  REGION: 지역
+  DESCRIPTION: 소개글
+  SNS_CHANNELS: SNS 채널 정보 (JSON)
+  FOLLOWER_COUNT: 팔로워 수
+  ENGAGEMENT_RATE: 참여율
+  IS_ACT: 활성화 상태
+  ```
+
+### 1.2 벤더사 (VENDOR_LIST)
+- 벤더사 정보는 VENDOR_LIST 테이블에서 관리
+- 주요 필드:
+  ```sql
+  SEQ: 고유 식별자
+  COMPANY_NAME: 회사명
+  EMAIL: 이메일
+  LOGO: 로고 이미지
+  CATEGORY: 업종 카테고리
+  REGION: 지역
+  DESCRIPTION: 회사 소개
+  IS_ACT: 활성화 상태
+  ```
+
+## 2. 파트너십 관리 (VENDOR_INFLUENCER_MAPPING)
+
+### 2.1 매핑 구조
+```sql
+SEQ: 매핑 고유 식별자
+VENDOR_SEQ: 벤더사 SEQ (VENDOR_LIST 참조)
+INFLUENCER_SEQ: 인플루언서 SEQ (USER_LIST 참조)
+STATUS: 상태 (PENDING/APPROVED/REJECTED/CANCELLED)
+REQUEST_TYPE: 요청 타입 (INFLUENCER_REQUEST/VENDOR_REQUEST)
+REQUEST_MESSAGE: 요청 메시지
+RESPONSE_MESSAGE: 응답 메시지
+COMMISSION_RATE: 수수료율
+REQUESTED_BY: 요청자 SEQ
+APPROVED_BY: 승인자 SEQ
+REQUEST_DATE: 요청일시
+RESPONSE_DATE: 응답일시
+IS_ACT: 활성화 상태
+```
+
+### 2.2 승인 프로세스
+1. 인플루언서 → 벤더사 승인 요청
+   - REQUEST_TYPE: 'INFLUENCER_REQUEST'
+   - REQUESTED_BY: 인플루언서 SEQ (USER_LIST)
+   - APPROVED_BY: 벤더사 담당자 SEQ (VENDOR_LIST)
+
+2. 벤더사 → 인플루언서 승인 요청
+   - REQUEST_TYPE: 'VENDOR_REQUEST'
+   - REQUESTED_BY: 벤더사 담당자 SEQ (VENDOR_LIST)
+   - APPROVED_BY: 인플루언서 SEQ (USER_LIST)
+
+## 3. 주의사항
+
+### 3.1 SEQ 참조 규칙
+- USER_LIST의 SEQ는 인플루언서 식별에만 사용
+- VENDOR_LIST의 SEQ는 벤더사 식별에만 사용
+- 두 테이블의 SEQ를 혼용하지 않음
+
+### 3.2 승인 처리 시 주의사항
+- APPROVED_BY는 항상 요청을 받은 쪽의 SEQ
+- REQUESTED_BY는 항상 요청을 보낸 쪽의 SEQ
+- STATUS 변경 시 요청 타입 확인 필수
+
+### 3.3 날짜 처리 규칙
+- 날짜 컬럼은 자동으로 처리됨 (INSERT/UPDATE 시 자동 설정)
+- REQUEST_DATE, RESPONSE_DATE는 수동으로 설정
+- 날짜 입력 시 NOW() 함수 사용
+- INTERVAL을 사용한 날짜 계산 가능 (예: NOW() - INTERVAL 30 DAY)
+description:
+globs:
+alwaysApply: false
+---

+ 123 - 0
.cursor/rules/sql-rules.mdc

@@ -0,0 +1,123 @@
+# SQL 작성 규칙
+
+## 1. DDL 작성 규칙
+
+### 1.1 컬럼 추가
+- ALTER TABLE 구문은 항상 단일 컬럼 단위로 분리하여 작성
+  ```sql
+  -- 좋은 예:
+  ALTER TABLE USER_LIST
+  ADD COLUMN REGION VARCHAR(50) COMMENT '지역' AFTER EMAIL;
+
+  ALTER TABLE USER_LIST
+  ADD COLUMN DESCRIPTION TEXT COMMENT '소개글' AFTER ENGAGEMENT_RATE;
+
+  -- 나쁜 예:
+  ALTER TABLE USER_LIST
+  ADD COLUMN REGION VARCHAR(50) COMMENT '지역' AFTER EMAIL,
+  ADD COLUMN DESCRIPTION TEXT COMMENT '소개글' AFTER ENGAGEMENT_RATE;
+  ```
+
+### 1.2 단계별 실행
+- 각 DDL 구문은 독립적으로 실행 가능하도록 작성
+- 주석으로 단계 구분을 명확히 표시
+  ```sql
+  -- 1단계: REGION 컬럼 추가
+  ALTER TABLE USER_LIST ...
+
+  -- 2단계: 인덱스 추가
+  ALTER TABLE USER_LIST ...
+  ```
+
+### 1.3 안전한 데이터 업데이트
+- WHERE 절 조건은 신중하게 설정
+  - 불필요한 제약조건 피하기 (예: USER_TYPE이나 PRIMARY_CATEGORY로 제한하지 않기)
+  - NULL 값 처리 로직 포함
+  ```sql
+  -- 좋은 예:
+  DESCRIPTION = CASE 
+      WHEN PRIMARY_CATEGORY IS NOT NULL THEN
+          CASE 
+              WHEN PRIMARY_CATEGORY = 'FASHION_BEAUTY' THEN '패션 크리에이터'
+              ELSE '일반 크리에이터'
+          END
+      ELSE '프로필 소개가 없습니다.'
+  END
+  ```
+
+## 2. MySQL 특화 문법
+
+### 2.1 문자열 연결
+- MySQL에서는 `||` 연산자 대신 CONCAT() 함수 사용
+  ```sql
+  -- 좋은 예:
+  CONCAT('prefix_', column_name, '_suffix')
+
+  -- 나쁜 예:
+  'prefix_' || column_name || '_suffix'
+  ```
+
+### 2.2 JSON 데이터 처리
+- JSON 문자열은 반드시 CONCAT() 함수로 조합
+- 이스케이프 문자 처리 주의
+  ```sql
+  -- 좋은 예:
+  CONCAT('{"key":"', value, '"}')
+
+  -- 나쁜 예:
+  '{"key":"' || value || '"}'
+  ```
+
+## 3. DBeaver 최적화
+
+### 3.1 실행 계획
+- 각 SQL 구문은 독립적으로 실행 가능하도록 분리
+- 트랜잭션이 필요한 경우 명시적으로 표시
+- 실행 순서를 주석으로 명확히 표시
+
+### 3.2 에러 처리
+- 컬럼 존재 여부 확인이 필요한 경우 IF NOT EXISTS 사용
+  ```sql
+  ALTER TABLE table_name
+  ADD COLUMN IF NOT EXISTS column_name data_type;
+  ```
+
+## 4. 데이터 마이그레이션
+
+### 4.1 데이터 업데이트
+- 대량 데이터 업데이트 시 LIMIT 사용 고려
+- 인덱스 영향도 고려
+- 롤백 계획 포함
+
+### 4.2 테스트 데이터
+- 의미 있는 테스트 데이터 생성
+- SEQ나 ID 값을 활용한 동적 데이터 생성
+- NULL 값 처리 로직 포함
+
+## 5. 성능 최적화
+
+### 5.1 인덱스
+- 조회 패턴에 맞는 인덱스 설계
+- 복합 인덱스 순서 고려
+- 불필요한 인덱스 제거
+
+### 5.2 쿼리 최적화
+- 대량 데이터 처리 시 배치 처리 고려
+- 임시 테이블 사용 최소화
+- 조인 성능 고려
+
+## 6. 문서화
+
+### 6.1 주석 처리
+- 각 SQL 구문의 목적 명시
+- 주의사항이나 제약사항 표시
+- 실행 순서와 종속성 명시
+
+### 6.2 백업 계획
+- 데이터 변경 전 백업 방법 명시
+- 롤백 스크립트 준비
+- 테스트 환경에서 검증 후 운영 반영
+description:
+globs:
+alwaysApply: false
+---

+ 62 - 2
.vooster/tasks.json

@@ -1,6 +1,6 @@
 {
-  "totalCount": 15,
-  "downloadedAt": "2025-07-21T06:42:29.250Z",
+  "totalCount": 21,
+  "downloadedAt": "2025-07-22T01:53:47.496Z",
   "tasks": [
     {
       "taskId": "T-001",
@@ -151,6 +151,66 @@
       "urgency": 8,
       "createdAt": "2025-07-21T06:24:11.558Z",
       "updatedAt": "2025-07-21T06:41:11.982Z"
+    },
+    {
+      "taskId": "T-016",
+      "summary": "벤더사-인플루언서 승인 매핑용 중계 테이블 및 API 설계/구현",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 7,
+      "urgency": 8,
+      "createdAt": "2025-07-22T01:48:43.838Z",
+      "updatedAt": "2025-07-22T01:48:43.838Z"
+    },
+    {
+      "taskId": "T-017",
+      "summary": "인플루언서 벤더사 검색 및 승인요청 UI/로직 구현",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 6,
+      "urgency": 8,
+      "createdAt": "2025-07-22T01:48:43.838Z",
+      "updatedAt": "2025-07-22T01:48:43.838Z"
+    },
+    {
+      "taskId": "T-018",
+      "summary": "벤더사 인플루언서 승인요청 리스트/승인처리 UI/로직 구현",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 6,
+      "urgency": 8,
+      "createdAt": "2025-07-22T01:48:43.838Z",
+      "updatedAt": "2025-07-22T01:48:43.838Z"
+    },
+    {
+      "taskId": "T-019",
+      "summary": "인플루언서 승인 상태에 따른 벤더사 제품 접근 제어",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 7,
+      "urgency": 7,
+      "createdAt": "2025-07-22T01:48:43.838Z",
+      "updatedAt": "2025-07-22T01:48:43.838Z"
+    },
+    {
+      "taskId": "T-020",
+      "summary": "승인요청 및 처리 내역/상태 조회 기능 추가",
+      "status": "BACKLOG",
+      "importance": "SHOULD",
+      "complexity": 5,
+      "urgency": 6,
+      "createdAt": "2025-07-22T01:48:43.838Z",
+      "updatedAt": "2025-07-22T01:48:43.838Z"
+    },
+    {
+      "taskId": "T-021",
+      "summary": "승인요청, 승인처리 시 실시간 피드백 및 알림 처리",
+      "status": "BACKLOG",
+      "importance": "SHOULD",
+      "complexity": 4,
+      "urgency": 5,
+      "createdAt": "2025-07-22T01:48:43.838Z",
+      "updatedAt": "2025-07-22T01:48:43.838Z"
     }
   ]
 }

+ 31 - 0
.vooster/tasks/T-016.txt

@@ -0,0 +1,31 @@
+# 벤더사-인플루언서 승인 매핑용 중계 테이블 및 API 설계/구현
+
+**Task ID:** T-016
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 7/10
+**Urgency:** 8/10
+**Dependencies:** None
+
+## Description
+
+# 설명
+- vendor_influencer_mapping 중계 테이블 및 approval_status 컬럼 설계 및 관련 API 구현
+
+## 구현 단계
+1. MySQL Migration 생성: vendor_influencer_mapping 테이블 정의(id, vendor_id, influencer_id, approval_status enum(PENDING,APPROVED,REJECTED), created_at, updated_at)
+2. CodeIgniter4 Model 및 Migration 클래스 작성
+3. Node.js BFF(Express)에서 RESTful 라우터 추가: POST /api/approval/request, POST /api/approval/handle, GET /api/approval/status
+4. Service 레이어 구현: 요청 생성, 승인/거부 상태 변경, 상태 조회 로직 작성
+5. Controller 계층 JWT 인증 및 입력 유효성 검증, 예외 처리 로직 추가
+6. OpenAPI 스펙 문서화 및 API 문서 업데이트
+
+## 테스트 전략
+- Migration 테스트: 테이블 생성 및 스키마 검증
+- 단위 테스트: Model CRUD 및 Service 상태 전이 시나리오 검증
+- 통합 테스트: API 호출 후 DB 반영 확인, 중복 요청 및 예외 케이스 검증
+
+---
+
+**Created:** 2025-07-22T01:48:43.838Z
+**Updated:** 2025-07-22T01:48:43.838Z

+ 30 - 0
.vooster/tasks/T-017.txt

@@ -0,0 +1,30 @@
+# 인플루언서 벤더사 검색 및 승인요청 UI/로직 구현
+
+**Task ID:** T-017
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 6/10
+**Urgency:** 8/10
+**Dependencies:** T-016
+
+## Description
+
+# 설명
+- pages/view/vendor/index.vue 또는 기존 그리드 페이지를 활용해 벤더사 목록 조회 및 승인요청 기능 구현
+
+## 구현 단계
+1. pages/view/vendor/index.vue 컴포넌트 생성/수정: 공통 그리드 컴포넌트 활용
+2. Axios GET /api/vendors API 연동해 벤더사 목록 불러오기
+3. 그리드 마지막 컬럼에 요청 상태(대기, 승인 완료) 표시 및 승인요청 버튼 배치
+4. 승인요청 버튼 클릭 이벤트: axios.post('/api/approval/request',{vendorId}) 호출 후 버튼 비활성화
+5. 요청 완료 또는 실패 시 toast/confirmDialog 컴포넌트로 피드백 제공
+6. 응답에 따라 그리드 데이터 갱신
+
+## 테스트 전략
+- 단위 테스트: 컴포넌트 렌더링, 버튼 상태 변경, API 호출 모킹
+- E2E 테스트: 실제 API 응답 시나리오(성공/오류) 시 그리드 업데이트 및 토스트 표시 확인
+
+---
+
+**Created:** 2025-07-22T01:48:43.838Z
+**Updated:** 2025-07-22T01:48:43.838Z

+ 30 - 0
.vooster/tasks/T-018.txt

@@ -0,0 +1,30 @@
+# 벤더사 인플루언서 승인요청 리스트/승인처리 UI/로직 구현
+
+**Task ID:** T-018
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 6/10
+**Urgency:** 8/10
+**Dependencies:** T-016
+
+## Description
+
+# 설명
+- pages/view/vendor/dashboard/index.vue 또는 공통 그리드 페이지를 활용해 인플루언서 승인 요청 관리 기능 구현
+
+## 구현 단계
+1. pages/view/vendor/dashboard/index.vue 컴포넌트 생성/수정: 공통 그리드 활용
+2. Axios GET /api/approval/requests?vendorId API 연동해 요청 리스트 조회
+3. 그리드 각 행에 승인/거부 버튼 추가
+4. 승인/거부 클릭 시 confirmDialog 호출 후 axios.post('/api/approval/handle',{mappingId,action}) 실행
+5. 요청 성공 시 그리드 해당 행 상태 업데이트 및 toast 피드백 제공
+6. 오류 발생 시 오류 토스트 표시 및 롤백 처리
+
+## 테스트 전략
+- 단위 테스트: 그리드 렌더링, 버튼 클릭 후 confirmDialog, API 모킹 테스트
+- 통합 테스트: 승인/거부 시나리오 전반 검증(상태 변경, UI 반영, 피드백)
+
+---
+
+**Created:** 2025-07-22T01:48:43.838Z
+**Updated:** 2025-07-22T01:48:43.838Z

+ 29 - 0
.vooster/tasks/T-019.txt

@@ -0,0 +1,29 @@
+# 인플루언서 승인 상태에 따른 벤더사 제품 접근 제어
+
+**Task ID:** T-019
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 7/10
+**Urgency:** 7/10
+**Dependencies:** T-016
+
+## Description
+
+# 설명
+- 승인된 인플루언서만 벤더사 제품 페이지에 접근하도록 제어 구현
+
+## 구현 단계
+1. Nuxt3 미들웨어 또는 라우트 가드 설정: /vendor/:id/products 접근 시 실행
+2. composables/useValid.js 또는 stores/auth.js 패턴 참고해 GET /api/approval/status?vendorId API 호출
+3. approval_status가 APPROVED가 아니면 "승인 필요" 메시지 또는 승인 요청 페이지로 리다이렉트
+4. APPROVED면 기존 벤더사 제품 리스트 컴포넌트 렌더링
+5. UI 컴포넌트 조건부 렌더링으로 접근 제어
+
+## 테스트 전략
+- 유닛 테스트: composable 및 미들웨어 로직 검증
+- 통합 테스트: 승인 전/후 상태에 따른 접근 결과 및 메시지 확인
+
+---
+
+**Created:** 2025-07-22T01:48:43.838Z
+**Updated:** 2025-07-22T01:48:43.838Z

+ 29 - 0
.vooster/tasks/T-020.txt

@@ -0,0 +1,29 @@
+# 승인요청 및 처리 내역/상태 조회 기능 추가
+
+**Task ID:** T-020
+**Status:** BACKLOG
+**Importance:** SHOULD
+**Complexity:** 5/10
+**Urgency:** 6/10
+**Dependencies:** T-016, T-017, T-018
+
+## Description
+
+# 설명
+- 인플루언서 마이페이지 및 벤더사 관리페이지에 승인 이력 조회 탭 추가
+
+## 구현 단계
+1. 인플루언서 마이페이지에 "나의 승인요청" 탭 추가, 공통 그리드 컴포넌트 활용
+2. Axios GET /api/approval/requests?userId API 연동해 요청 이력 조회(대기/완료/거부 필터링)
+3. 벤더사 관리페이지에도 "승인 처리 이력" 탭 추가, 동일 그리드 활용
+4. 상태별 색상 표기 및 정렬 기능 추가
+5. API 호출 실패 시 오류 메시지 처리
+
+## 테스트 전략
+- 단위 테스트: 탭 전환, API 응답 모킹, 그리드 데이터 렌더링 검증
+- E2E 테스트: 필터링, 정렬, 페이지 내비게이션 검증
+
+---
+
+**Created:** 2025-07-22T01:48:43.838Z
+**Updated:** 2025-07-22T01:48:43.838Z

+ 29 - 0
.vooster/tasks/T-021.txt

@@ -0,0 +1,29 @@
+# 승인요청, 승인처리 시 실시간 피드백 및 알림 처리
+
+**Task ID:** T-021
+**Status:** BACKLOG
+**Importance:** SHOULD
+**Complexity:** 4/10
+**Urgency:** 5/10
+**Dependencies:** T-017, T-018
+
+## Description
+
+# 설명
+- 승인요청 및 승인처리 시 toast/confirmDialog 외 웹소켓 기반 실시간 알림 연동 보강
+
+## 구현 단계
+1. 공통 toast 및 confirmDialog 컴포넌트 재사용해 기본 피드백 구현 확인
+2. 웹소켓(socket.io) 클라이언트 설정: Nuxt3 plugin에 등록
+3. 서버에서 approvalStatusChanged 이벤트 발행, 클라이언트 구독 로직 작성
+4. 알림센터 컴포넌트에 수신된 이벤트 표시(UI 배지, 목록 추가)
+5. 필요 시 푸시 알림 센터 API 연동
+
+## 테스트 전략
+- 유닛 테스트: socket 연결, 이벤트 핸들러 로직 검증
+- 통합 테스트: 서버 이벤트 시뮬레이션, 클라이언트 알림 수신 및 UI 업데이트 확인
+
+---
+
+**Created:** 2025-07-22T01:48:43.838Z
+**Updated:** 2025-07-22T01:48:43.838Z

+ 135 - 0
backend/README.md

@@ -0,0 +1,135 @@
+# Influence Backend API
+
+기존 동작하는 코드를 기반으로 구성된 벤더-인플루언서 관계 관리 API
+
+## 프로젝트 구조
+
+```
+backend/
+├── app/
+│   ├── Config/
+│   │   └── Routes.php              # API 라우트 설정
+│   ├── Controllers/
+│   │   └── VendorInfluencerController.php  # 메인 컨트롤러
+│   └── Models/
+│       ├── VendorInfluencerMappingModel.php  # 벤더-인플루언서 매핑 모델
+│       ├── VendorModel.php         # 벤더사 모델
+│       └── UserModel.php           # 사용자 모델
+└── README.md
+```
+
+## API 엔드포인트
+
+### 벤더-인플루언서 관계 관리
+- `POST /api/vendor-influencer/request` - 승인요청 생성
+- `POST /api/vendor-influencer/requests` - 요청목록 조회 (벤더사용)
+- `POST /api/vendor-influencer/process` - 승인/거부 처리
+- `POST /api/vendor-influencer/cancel` - 요청취소
+- `POST /api/vendor-influencer/list` - 관계목록 조회 (인플루언서용)
+- `POST /api/vendor-influencer/detail` - 상세 조회
+
+### 벤더사 검색
+- `POST /api/vendor/search` - 벤더사 검색 (인플루언서용)
+
+## 주요 기능
+
+### 1. 벤더사 검색 (`searchVendors`)
+- 키워드, 카테고리, 지역별 검색
+- 정렬: latest, partnership, name
+- 페이징 지원
+- 파트너십 상태 확인
+
+### 2. 승인요청 생성 (`createRequest`)
+- 중복 요청 방지
+- 만료일 자동 설정 (7일)
+- 요청 메시지, 수수료율, 특별조건 포함
+
+### 3. 승인요청 목록 조회 (`getList`)
+- 벤더사/인플루언서별 필터링
+- 키워드 검색, 카테고리 필터
+- 정렬: latest, oldest, expiring
+- 통계 정보 제공 (pending, approved, rejected, total)
+
+### 4. 승인/거부 처리 (`approveRequest`)
+- action 파라미터로 APPROVE/REJECT 구분
+- 응답 메시지, 수수료율 설정
+- 처리자 및 처리일시 기록
+
+### 5. 요청 취소 (`cancelRequest`)
+- 권한 확인
+- 취소 사유 기록
+
+### 6. 상세 조회 (`getDetail`)
+- 벤더사, 인플루언서 정보 포함
+- 매핑 정보 상세 조회
+
+## 데이터베이스 테이블
+
+### VENDOR_INFLUENCER_MAPPING
+- 벤더사-인플루언서 관계 매핑
+- 승인요청, 상태, 메시지, 수수료율 등
+
+### VENDOR_LIST
+- 벤더사 정보
+- 회사명, 카테고리, 지역, 로고 등
+
+### USER_LIST
+- 사용자 정보 (인플루언서 포함)
+- 회원유형, 프로필, 카테고리 등
+
+## 기존 코드 호환성
+
+이 API는 기존 동작하는 코드와 100% 호환됩니다:
+- 기존 메서드 시그니처 유지
+- 동일한 요청/응답 포맷
+- 기존 데이터베이스 구조 활용
+- 추가 기능만 확장
+
+## 사용 예시
+
+### 벤더사 검색
+```javascript
+const response = await fetch('/api/vendor/search', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+        keyword: '패션',
+        category: 'FASHION_BEAUTY',
+        region: 'SEOUL',
+        sortBy: 'latest',
+        page: 1,
+        size: 12,
+        influencerSeq: 123
+    })
+});
+```
+
+### 승인요청 목록 조회 (벤더사용)
+```javascript
+const response = await fetch('/api/vendor-influencer/requests', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+        vendorSeq: 456,
+        status: 'PENDING',
+        keyword: '인플루언서명',
+        sortBy: 'latest',
+        page: 1,
+        size: 12
+    })
+});
+```
+
+### 승인 처리
+```javascript
+const response = await fetch('/api/vendor-influencer/process', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+        mappingSeq: 789,
+        action: 'APPROVE', // or 'REJECT'
+        processedBy: 456,
+        responseMessage: '승인합니다.'
+    })
+});
+```

+ 186 - 0
backend/app/Config/Routes.php

@@ -0,0 +1,186 @@
+<?php
+
+use CodeIgniter\Router\RouteCollection;
+
+/**
+ * @var RouteCollection $routes
+ */
+$routes->get('auth/googleLogin', 'Auth::googleLogin');
+$routes->get('auth/callback', 'Auth::callback');
+$routes->post('auth/joinmember', 'Auth::join');
+$routes->post('auth/joinvendor', 'Auth::joinVendor');
+$routes->post('auth/withdrawal', 'Auth::withdrawal'); //구글 회원탈퇴 , 일반회원 탈퇴
+$routes->post('auth/kakaowithdrawal', 'Auth::kakaoWithdrawal'); //카카오 회웥탈퇴
+$routes->get('auth/kakaoLogin', 'Auth::kakaoLogin');
+$routes->get('auth/kakao', 'Auth::kakao');
+$routes->get('auth/naverLogin', 'Auth::naverLogin');
+$routes->get('auth/naver', 'Auth::naver');
+$routes->get('auth/naverWithdrawal', 'Auth::naverWithdrawal');
+$routes->post('auth/checkId', 'Auth::checkId'); // 사용 중 체크 아이디
+
+$routes->get('/', 'Home::index'); //홈화면 리다이렉트용
+$routes->post('roulette/login', 'Roulette::login'); //로그인 페이지 토큰 상관없이 호출가능
+$routes->post('roulette/refreshToken', 'Roulette::refreshToken'); //엑세스 토큰이 있어야만 발급 가능
+
+$routes->get('alimtalk/send', 'Alimtalk::send');
+$routes->post('alimtalk/send', 'Alimtalk::send'); // POST 요청인 경우
+
+$routes->post('winner/reg', 'Winner::winnerReg');
+$routes->post('winner/itemcount', 'Winner::itemCount');
+$routes->post('winner/winnerchk', 'Winner::winnerChk');
+
+// 관리자 라우트
+$routes->post('mng/list', 'Mng::mnglist');
+$routes->post('mng/search', 'Mng::mngSearch');
+$routes->post('mng/reg', 'Mng::mngRegister');
+$routes->post('mng/chk', 'Mng::mngIDChk');
+$routes->post('mng/update', 'Mng::mngUpdate');
+$routes->get('mng/detail/(:segment)', 'Mng::mngDetail/$1');
+$routes->post('mng/stupdate/(:segment)', 'Mng::mngStatusUpdate/$1');
+$routes->post('mng/delete/(:segment)', 'Mng::mngDelete/$1');
+
+// 아이템 라우트
+$routes->post('item/list', 'Item::itemlist');
+$routes->post('item/reg', 'Item::itemRegister');
+$routes->get('item/detail/(:num)', 'Item::itemDetail/$1');
+$routes->post('item/update/(:num)', 'Item::itemUpdate/$1');
+$routes->post('item/delete/(:num)', 'Item::itemDelete/$1');
+$routes->post('item/search', 'Item::itemSearch');
+// 파일 다운로드
+$routes->get('item/download/(:segment)', 'Item::file/$1');
+
+// 당첨자 라우트
+$routes->post('winner/list', 'Winner::winnerlist');
+$routes->get('winner/detail/(:num)', 'Winner::winnerDetail/$1');
+$routes->post('winner/partclist', 'Winner::getParticipationByItem');
+$routes->post('winner/matcheduser', 'Winner::matchedUser');
+
+$routes->group('', ['filter' => 'auth'], function ($routes) {
+});
+
+// API 라우트 그룹
+$routes->group('api', ['namespace' => 'App\Controllers'], function($routes) {
+  
+  // 벤더사 관련 API
+  $routes->group('vendor', function($routes) {
+    $routes->post('search', 'VendorInfluencerController::searchVendors');
+    $routes->post('list', 'VendorController::getList');
+    $routes->post('detail', 'VendorController::getDetail');
+    $routes->post('create', 'VendorController::create');
+    $routes->post('update', 'VendorController::update');
+    $routes->post('delete', 'VendorController::delete');
+  });
+  
+  // 벤더사-인플루언서 매핑 관련 API
+  $routes->group('vendor-influencer', function($routes) {
+    $routes->post('request', 'VendorInfluencerController::createRequest');
+    $routes->post('requests', 'VendorInfluencerController::getList');         // 요청목록 조회 (벤더사용)
+    $routes->post('approve', 'VendorInfluencerController::approveRequest');
+    $routes->post('process', 'VendorInfluencerController::approveRequest');   // 승인/거부 처리 (통합)
+    $routes->post('list', 'VendorInfluencerController::getList');
+    $routes->post('detail', 'VendorInfluencerController::getDetail');
+    $routes->post('cancel', 'VendorInfluencerController::cancelRequest');
+    $routes->post('stats', 'VendorInfluencerController::getStats');
+    $routes->post('history/(:num)', 'VendorInfluencerController::getHistory/$1');
+  });
+  
+  // 인증 관련 API
+  $routes->group('auth', function($routes) {
+    $routes->post('login', 'AuthController::login');
+    $routes->post('logout', 'AuthController::logout');
+    $routes->post('register', 'AuthController::register');
+    $routes->post('refresh', 'AuthController::refreshToken');
+    $routes->post('verify', 'AuthController::verifyToken');
+  });
+  
+  // 사용자 관련 API
+  $routes->group('user', function($routes) {
+    $routes->post('profile', 'UserController::getProfile');
+    $routes->post('update-profile', 'UserController::updateProfile');
+    $routes->post('change-password', 'UserController::changePassword');
+    $routes->post('upload-avatar', 'UserController::uploadAvatar');
+  });
+  
+  // 제품 관련 API
+  $routes->group('item', function($routes) {
+    $routes->post('list', 'ItemController::getList');
+    $routes->post('detail', 'ItemController::getDetail');
+    $routes->post('create', 'ItemController::create');
+    $routes->post('update', 'ItemController::update');
+    $routes->post('delete', 'ItemController::delete');
+    $routes->post('search', 'ItemController::search');
+  });
+  
+  // 파일 업로드 관련 API
+  $routes->group('upload', function($routes) {
+    $routes->post('image', 'UploadController::uploadImage');
+    $routes->post('file', 'UploadController::uploadFile');
+    $routes->post('multiple', 'UploadController::uploadMultiple');
+  });
+  
+  // 알림 관련 API
+  $routes->group('notification', function($routes) {
+    $routes->post('list', 'NotificationController::getList');
+    $routes->post('mark-read', 'NotificationController::markAsRead');
+    $routes->post('mark-all-read', 'NotificationController::markAllAsRead');
+    $routes->post('delete', 'NotificationController::delete');
+  });
+  
+  // 대시보드 관련 API
+  $routes->group('dashboard', function($routes) {
+    $routes->post('stats', 'DashboardController::getStats');
+    $routes->post('recent-activities', 'DashboardController::getRecentActivities');
+    $routes->post('chart-data', 'DashboardController::getChartData');
+  });
+});
+
+// 인증이 필요한 API 라우트 (필터 적용)
+$routes->group('api', ['namespace' => 'App\Controllers', 'filter' => 'auth'], function($routes) {
+  
+  // 보호된 벤더사-인플루언서 API
+  $routes->group('vendor-influencer/protected', function($routes) {
+    $routes->post('my-requests', 'VendorInfluencerController::getMyRequests');
+    $routes->post('my-partnerships', 'VendorInfluencerController::getMyPartnerships');
+    $routes->post('pending-approvals', 'VendorInfluencerController::getPendingApprovals');
+  });
+  
+  // 관리자 전용 API
+  $routes->group('admin', ['filter' => 'admin'], function($routes) {
+    $routes->post('vendor-influencer/all', 'AdminController::getAllMappings');
+    $routes->post('vendor-influencer/expired', 'AdminController::getExpiredRequests');
+    $routes->post('vendor-influencer/process-expired', 'AdminController::processExpiredRequests');
+    $routes->post('system/stats', 'AdminController::getSystemStats');
+  });
+});
+
+// 웹훅 및 외부 API
+$routes->group('webhook', ['namespace' => 'App\Controllers'], function($routes) {
+  $routes->post('payment/success', 'WebhookController::paymentSuccess');
+  $routes->post('payment/failure', 'WebhookController::paymentFailure');
+  $routes->post('notification/send', 'WebhookController::sendNotification');
+});
+
+// 크론잡 및 스케줄러 API
+$routes->group('cron', ['namespace' => 'App\Controllers', 'filter' => 'cron'], function($routes) {
+  $routes->get('process-expired-requests', 'CronController::processExpiredRequests');
+  $routes->get('send-reminder-notifications', 'CronController::sendReminderNotifications');
+  $routes->get('cleanup-old-data', 'CronController::cleanupOldData');
+});
+
+// 개발 및 테스트용 라우트 (개발 환경에서만 사용)
+if (ENVIRONMENT === 'development') {
+  $routes->group('dev', ['namespace' => 'App\Controllers'], function($routes) {
+    $routes->get('test-db', 'DevController::testDatabase');
+    $routes->get('seed-data', 'DevController::seedTestData');
+    $routes->get('clear-cache', 'DevController::clearCache');
+    $routes->post('test-api', 'DevController::testApi');
+  });
+}
+
+// 디버깅용 라우트 (임시)
+$routes->group('debug', ['namespace' => 'App\\Controllers'], function($routes) {
+  $routes->get('foreign-key', 'DebugController::debugForeignKey');
+  $routes->get('simple-update', 'DebugController::testSimpleUpdate');
+});
+
+$routes->post('api/influencer/profile', 'InfluencerController::getProfile');

+ 911 - 0
backend/app/Controllers/Auth.php

@@ -0,0 +1,911 @@
+<?php
+  
+  namespace App\Controllers;
+  require_once __DIR__ . '/../Libraries/autoload.php';
+  
+  use App\Models\UserListModel;
+  use CodeIgniter\RESTful\ResourceController;
+  use Google\Client;
+  use Google\Service\Oauth2;
+  use App\Libraries\JwtLib\JWT;
+  use App\Libraries\JwtLib\Key;
+  
+ 
+  class Auth extends ResourceController
+  {
+    private const FRONTEND_BASE_URL = "http://localhost:3000";
+    
+  //private const FRONTEND_BASE_URL = 'https://shopdeli.mycafe24.com';
+    protected $userModel;
+    
+    public function __construct()
+    {
+      $this->userModel = new UserListModel();
+    }
+    
+    //구글 로그인 콜백(인증)
+    public function callback()
+    {
+      if (isset($_GET['code'])) {
+        $code = $_GET['code'];
+        
+        // Google 클라이언트 설정
+        $client = new Client();
+        $client->setClientId('373780605211-diojebh7mug45urv9rnqdil6n0b1ogge.apps.googleusercontent.com');
+        $client->setClientSecret('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');
+        $client->setRedirectUri('https://shopdeli.mycafe24.com/auth/callback');
+        $client->setAccessType('offline'); // 인증 요청 전에 지정!
+        $client->authenticate($code);
+        $googleAccessToken = $client->getAccessToken();
+
+        // 별도의 변수로 분리
+        $googleOAuthAccessToken = $googleAccessToken['access_token'] ?? null;
+        $googleOAuthRefreshToken = $googleAccessToken['refresh_token'] ?? null;
+        
+        
+        // 업데이트: setAccessToken에는 전체 배열을 넣을 수 있지만
+        // accessToken 만 넣으려면 아래처럼 쓸 수 있습니다.
+        if ($googleOAuthAccessToken) {
+          $client->setAccessToken($googleOAuthAccessToken);
+        }
+        
+        
+        
+        // 사용자 정보 가져오기
+        $oauth2 = new Oauth2($client);
+        $userInfo = $oauth2->userinfo->get();
+        
+        // 사용자 정보 변수 지정
+        $id = $userInfo->id;
+        $authenticatedEmail = $userInfo->email;
+        $name = $userInfo->name;
+        
+        
+        // DB Connection (CodeIgniter 4)
+        $db = \Config\Database::connect();
+        
+        // 1. USER_LIST에서 이메일로 조회
+        $builder = $db->table('USER_LIST');
+        $user = $builder->where('EMAIL', $authenticatedEmail)->get()->getRowArray();
+        
+        if ($user) {
+          
+          $jwtSecret = env('JWT_SECRET');
+          $kid = env('JWT_KID');
+          
+          if (empty($jwtSecret) || empty($kid)) {
+            return $this->failServerError('환경변수가 누락되었습니다. 관리자에게 문의하세요.');
+          }
+          if (!class_exists('\App\Libraries\JwtLib\JWT')) {
+            return $this->failServerError('JWT 라이브러리를 찾을 수 없습니다.');
+          }
+          
+          $issuedAt = time();
+          $accessExpire = $issuedAt + 60 * 15; // 15분
+          $refreshExpire = $issuedAt + 60 * 60 * 24 * 14; // 14일
+          
+          $accessPayload = [
+            'iat' => $issuedAt,
+            'exp' => $accessExpire,
+            'sub' => $id,
+            'name' => $name ?? '',
+          ];
+          
+          $refreshPayload = [
+            'iat' => $issuedAt,
+            'exp' => $refreshExpire,
+            'sub' => $id,
+          ];
+          
+          try {
+            $accessToken = JWT::encode($accessPayload, $jwtSecret, 'HS256', $kid);
+            $refreshToken = JWT::encode($refreshPayload, $jwtSecret, 'HS256', $kid);
+            
+            // (선택) DB의 리프레시 토큰 값 업데이트
+            $db = \Config\Database::connect();
+            $builder = $db->table('USER_LIST');
+            $builder->where('ID', $id)->update(['REFRESH_TOKEN' => $refreshToken]);
+            
+          } catch (\Throwable $e) {
+            return $this->failServerError('JWT 생성 오류: ' . $e->getMessage());
+          }
+          
+          unset($user['PASSWORD']); // 혹시 비번이 있다면 노출 방지
+          
+          $query = http_build_query([
+            "accessToken" => $accessToken,
+            "refreshToken" => $refreshToken,
+            "user" => urlencode(json_encode($user)),
+          ]);
+          
+          //return redirect()->to(base_url("auth/popupClose?$query"));
+          // 개발진행중 풀 URL
+          return redirect()->to(self::FRONTEND_BASE_URL."/auth/popupClose?$query");
+          
+          
+        } else {
+          // 회원이 없으면 회원가입
+          $type = 1;
+          
+          // ID는 auto_increment라면 생략
+          $authData = [
+            'ID' => $id,
+            'NAME' => $name,
+            'EMAIL' => $authenticatedEmail,
+            'TYPE' => $type,
+            'JOIN' => '1',
+            'GOOGLE_REFRESH_TOKEN' => $googleOAuthRefreshToken ?? null
+          ];
+          
+          $query = http_build_query([
+            "user" => urlencode(json_encode($authData)),
+          ]);
+          
+          
+          //return redirect()->to(base_url("auth/popupClose?$query"));
+          // 개발진행중 풀 URL
+          return redirect()->to(self::FRONTEND_BASE_URL."/auth/popupClose?$query");
+          
+          
+        }
+      } else {
+        echo "인증코드가 없습니다.";
+      }
+    }
+    
+    
+    public function join()
+    {
+      $postData = $this->request->getJSON(true);
+      
+      
+      // 필수값 추출
+      $id = $postData['ID'] ?? null;
+      $password = $postData['PASSWORD'] ?? null;
+      $name = $postData['NAME'] ?? null;
+      $nick_name = $postData['NICK_NAME'] ?? null;
+      $phone = $postData['PHONE'] ?? null;
+      $email = $postData['EMAIL'] ?? null;
+      $sns_type = $postData['SNS_TYPE'] ?? null;
+      $sns_link_id = $postData['SNS_LINK_ID'] ?? null;
+      $add_info1 = $postData['ADD_INFO1'] ?? null;
+      $google_refresh_token = $postData['GOOGLE_REFRESH_TOKEN'] ?? null;
+      $type = $postData['TYPE'] ?? null;
+    
+      
+      // 필수값 검증
+      if (empty($name) || empty($email)) {
+        return redirect()->back()->with('error', '필수 정보를 입력해 주세요.');
+      }
+      
+      // insert용 데이터 준비
+      $insertData = [
+        'ID' => $id,
+        'PASSWORD' => $password,
+        'NAME' => $name,
+        'NICK_NAME' => $nick_name,
+        'PHONE' => $phone,
+        'EMAIL' => $email,
+        'SNS_TYPE' => $sns_type,
+        'SNS_LINK_ID' => $sns_link_id,
+        'ADD_INFO1' => $add_info1,
+        'GOOGLE_REFRESH_TOKEN' => $google_refresh_token,
+        'TYPE'  => $type,
+      ];
+      
+      if (!empty($password)) {
+        $insertData['PASSWORD'] = password_hash($password, PASSWORD_DEFAULT);
+      }
+      
+      // DB 연결 및 INSERT
+      try {
+        $db = \Config\Database::connect();
+        $builder = $db->table('USER_LIST');
+        $result = $builder->insert($insertData);
+        
+        if (!$result) {
+          $error = $db->error();
+          return $this->response->setJSON([
+            'status' => 'fail',
+            'message' => 'DB Error: '.$error['message']
+          ])->setStatusCode(500);
+        }
+      } catch (\Throwable $e) {
+        return $this->response->setJSON([
+          'status' => 'fail',
+          'message' => 'Exception: ' . $e->getMessage()
+        ])->setStatusCode(500);
+      }
+      
+      
+      // 회원가입 성공 시, JSON 응답으로 결과 반환 (200 OK)
+      return $this->response->setJSON([
+        'status'  => 'success',
+        'message' => '회원가입이 완료되었습니다.'
+      ])->setStatusCode(200);
+    }
+    
+    
+    public function joinVendor()
+    {
+      $postData = $this->request->getJSON(true);
+      
+      
+      // 필수값 추출
+      $id = $postData['ID'] ?? null;
+      $password = $postData['PASSWORD'] ?? null;
+      $name = $postData['NAME'] ?? null;
+      $company_name = $postData['COMPANY_NAME'] ?? null;
+      $company_number = $postData['COMPANY_NUMBER'] ?? null;
+      $hp = $postData['HP'] ?? null;
+      $email = $postData['EMAIL'] ?? null;
+      
+      
+      
+      // 필수값 검증
+      if (empty($name) || empty($email)) {
+        return redirect()->back()->with('error', '필수 정보를 입력해 주세요.');
+      }
+      
+      // insert용 데이터 준비
+      $insertData = [
+        'ID' => $id,
+        'PASSWORD' => $password,
+        'NAME' => $name,
+        'COMPANY_NAME' => $company_name,
+        'HP' => $hp,
+        'EMAIL' => $email,
+        'COMPANY_NUMBER' => $company_number
+      ];
+      
+      if (!empty($password)) {
+        $insertData['PASSWORD'] = password_hash($password, PASSWORD_DEFAULT);
+      }
+      
+      // DB 연결 및 INSERT
+      try {
+        $db = \Config\Database::connect();
+        $builder = $db->table('VENDOR_LIST');
+        $result = $builder->insert($insertData);
+        
+        if (!$result) {
+          $error = $db->error();
+          return $this->response->setJSON([
+            'status' => 'fail',
+            'message' => 'DB Error: '.$error['message']
+          ])->setStatusCode(500);
+        }
+      } catch (\Throwable $e) {
+        return $this->response->setJSON([
+          'status' => 'fail',
+          'message' => 'Exception: ' . $e->getMessage()
+        ])->setStatusCode(500);
+      }
+      
+      
+      // 회원가입 성공 시, JSON 응답으로 결과 반환 (200 OK)
+      return $this->response->setJSON([
+        'status'  => 'success',
+        'message' => '회원가입이 완료되었습니다.'
+      ])->setStatusCode(200);
+    }
+    
+    
+    
+    //구글 로그인 환경 회원 탈퇴
+    public function withdrawal()
+    {
+      
+      
+      // 1. 요청에서 사용자 SEQ 추출 (예: POST or GET)
+      $postData = $this->request->getJSON(true);
+      $seq = $postData['SEQ'];
+      $googleAccessToken = $postData['GOOGLE_REFRESH_TOKEN'];
+      // 2. 사용자 정보 조회 (구글 토큰 포함)
+      $user = $this->userModel->find($seq);
+      
+      if (!$user) {
+        return $this->response->setJSON(['error' => 'User not found'])->setStatusCode(404);
+      }
+      
+      // 3. 구글 인증 연결 해제(access_token 필요)
+      if ($googleAccessToken) {
+        $this->revokeGoogleAccess($googleAccessToken);
+      }
+      
+      // 4. USER_LIST에서 사용자 삭제
+      $this->userModel->delete($seq);
+      
+      // 5. 응답 반환
+      return $this->response->setJSON([
+        'result' => 'User account deleted and Google link revoked'
+      ])->setStatusCode(200);
+    }
+    
+    
+    
+    /***********************************
+     * 카카오 간편로그인 / 가입
+     **********************************/
+    
+    //카카오 인증
+    public function kakaoLogin()
+    {
+      $clientId = '1f8376b18a02a00f2e4e5594f9ace6d4'; // 카카오 REST API 키로 변경
+      $redirectUri = urlencode('https://shopdeli.mycafe24.com/auth/kakao'); // 콜백 URL (ex: https://도메인/auth/kakaoCallback)
+      
+      $url = "https://kauth.kakao.com/oauth/authorize?client_id={$clientId}&redirect_uri={$redirectUri}&response_type=code";
+      return redirect()->to($url);
+    }
+    
+    //카카오 인증후 진행
+    public function kakao()
+    {
+      $code = $this->request->getGet('code');
+      
+      $clientId = '1f8376b18a02a00f2e4e5594f9ace6d4'; // 카카오 REST API 키
+      $redirectUri = 'https://shopdeli.mycafe24.com/auth/kakao';    // 콜백 URL
+      $tokenUrl = "https://kauth.kakao.com/oauth/token";
+      
+      // 토큰 요청
+      $tokenData = [
+        'grant_type'    => 'authorization_code',
+        'scopes'        => 'offline_access',
+        'client_id'     => $clientId,
+        'redirect_uri'  => $redirectUri,
+        'code'          => $code,
+      ];
+      
+      $client = \Config\Services::curlrequest();
+      $tokenResponse = $client->post($tokenUrl, [
+        'form_params' => $tokenData
+      ]);
+      
+      $tokenResult = json_decode($tokenResponse->getBody(), true);
+      $accessTokenKakao = $tokenResult['access_token'];
+      $refreshTokenKakao = $tokenResult['refresh_token'];
+      
+      
+      
+      // 사용자 정보 요청
+      $userUrl = "https://kapi.kakao.com/v2/user/me";
+      $userResponse = $client->get($userUrl, [
+        'headers' => [
+          'Authorization' => 'Bearer ' . $accessTokenKakao,
+        ]
+      ]);
+      
+      $userInfo = json_decode($userResponse->getBody(), true);
+      $userInfo['access_token'] = $accessTokenKakao;
+      $userInfo['refresh_token'] = $refreshTokenKakao; //회원탈퇴시 이용 로그인시 마다 신규 리프래시 토큰 발급 받게 됨
+      
+      // 여기에 회원가입/로그인 처리를 구현하세요
+      // 예: $userInfo['kakao_account']['email'], $userInfo['properties']['nickname']
+      
+      
+      //DB 커넥션
+      $db = \Config\Database::connect();
+      
+      // 1. USER_LIST에서 이메일로 조회
+      $id = $userInfo['id'];
+      $email = $userInfo['kakao_account']['email'];
+      $builder = $db->table('USER_LIST');
+      $user = $builder->where('EMAIL', $email)->get()->getRowArray();
+      
+      
+      if($user){
+        $jwtSecret = env('JWT_SECRET');
+        $kid = env('JWT_KID');
+        
+        if (empty($jwtSecret) || empty($kid)) {
+          return $this->failServerError('환경변수가 누락되었습니다. 관리자에게 문의하세요.');
+        }
+        if (!class_exists('\App\Libraries\JwtLib\JWT')) {
+          return $this->failServerError('JWT 라이브러리를 찾을 수 없습니다.');
+        }
+        
+        $issuedAt = time();
+        $accessExpire = $issuedAt + 60 * 15; // 15분
+        $refreshExpire = $issuedAt + 60 * 60 * 24 * 14; // 14일
+        
+        $accessPayload = [
+          'iat' => $issuedAt,
+          'exp' => $accessExpire,
+          'sub' => $id,
+          //'name' => $name ?? '',
+        ];
+        
+        $refreshPayload = [
+          'iat' => $issuedAt,
+          'exp' => $refreshExpire,
+          'sub' => $id,
+        ];
+        
+        try {
+          $accessToken = JWT::encode($accessPayload, $jwtSecret, 'HS256', $kid);
+          $refreshToken = JWT::encode($refreshPayload, $jwtSecret, 'HS256', $kid);
+          
+          // (선택) DB의 리프레시 토큰 값 업데이트
+          $db = \Config\Database::connect();
+          $builder = $db->table('USER_LIST');
+          $builder->where('ID', $id)->update([
+            'REFRESH_TOKEN' => $refreshToken,
+            'KAKAO_REFRESH_TOKEN' => $userInfo['refresh_token']
+          ]);
+          
+        } catch (\Throwable $e) {
+          return $this->failServerError('JWT 생성 오류: ' . $e->getMessage());
+        }
+        
+        unset($user['PASSWORD']); // 혹시 비번이 있다면 노출 방지
+        
+        $query = http_build_query([
+          "accessToken" => $accessToken,
+          "refreshToken" => $refreshToken,
+          "user" => urlencode(json_encode($user)),
+        ]);
+        
+        //return redirect()->to(base_url("auth/popupClose?$query"));
+        // 개발진행중 풀 URL
+        return redirect()->to(self::FRONTEND_BASE_URL."/auth/popupClose?$query");
+      }else{
+        // 회원이 없으면 회원가입
+        $type = 1;
+        
+        // ID는 auto_increment라면 생략
+        $authData = [
+          'ID' => $id,
+          //'NAME' => $name,
+          'EMAIL' => $email,
+          'TYPE' => $type,
+          'JOIN' => '1',
+          'KAKAO_REFRESH_TOKEN' => $userInfo['refresh_token'] ?? null
+        ];
+        
+        $query = http_build_query([
+          "user" => urlencode(json_encode($authData)),
+        ]);
+        
+        
+        //return redirect()->to(base_url("auth/popupClose?$query"));
+        // 개발진행중 풀 URL
+        return redirect()->to(self::FRONTEND_BASE_URL."/auth/popupClose?$query");
+      }
+      
+      
+    }
+    
+    //카카오 회원탈퇴
+    public function kakaoWithdrawal(){
+      
+      
+      // 1. 요청에서 사용자 SEQ 추출 (예: POST or GET)
+      $postData = $this->request->getJSON(true);
+      $seq = $postData['SEQ'];
+      
+      $refreshTokenKaKao = $postData['KAKAO_REFRESH_TOKEN'];
+      
+      // 2. 사용자 정보 조회 (구글 토큰 포함)
+      $user = $this->userModel->find($seq);
+      
+      if (!$user) {
+        return $this->response->setJSON(['error' => 'User not found'])->setStatusCode(404);
+      }
+      
+      // 3. 카카오 언링크 처리
+      
+      $tokenUrl = "https://kauth.kakao.com/oauth/token";
+      $tokenData = [
+        'grant_type'    => 'refresh_token',
+        'client_id'     => '1f8376b18a02a00f2e4e5594f9ace6d4',
+        'refresh_token' => $refreshTokenKaKao,  // DB에서 불러온 값
+      ];
+      
+      $client = \Config\Services::curlrequest();
+      $tokenResponse = $client->post($tokenUrl, [
+        'form_params' => $tokenData
+      ]);
+      $tokenResult = json_decode($tokenResponse->getBody(), true);
+      $accessTokenKakao = $tokenResult['access_token'];
+
+      // (2) 발급받은 access_token으로 연결 끊기 요청
+      $userUnlinkUrl = "https://kapi.kakao.com/v1/user/unlink";
+      $response = $client->post($userUnlinkUrl, [
+        'headers' => [
+          'Authorization' => 'Bearer ' . $accessTokenKakao,
+        ]
+      ]);
+      
+      // 4. USER_LIST에서 사용자 삭제
+      $this->userModel->delete($seq);
+      
+      // 5. 응답 반환
+      return $this->response->setJSON([
+        'result' => 'User account deleted and Google link revoked'
+      ])->setStatusCode(200);
+    }
+    
+    
+    
+    /***********************************
+     * 내아버 간편로그인 / 가입
+     **********************************/
+    
+    //네이버 인증
+    public function naverLogin()
+    {
+      $client_id = 'tPw7dRu1r7yY89O5gN7n';
+      $redirect_uri = urlencode('https://shopdeli.mycafe24.com/auth/naver'); // ex) https://your.site/naver-callback
+      $state = bin2hex(random_bytes(8));
+      session()->set('naver_state', $state);
+      
+      $naver_auth_url = "https://nid.naver.com/oauth2.0/authorize?"
+        . "response_type=code"
+        . "&client_id={$client_id}"
+        . "&client_icon=https://ndevthumb-phinf.pstatic.net/20250708_43/1751954347202gc3db_JPEG/x49C3CDfcWcI20250708145907.jpeg"
+        . "&redirect_uri={$redirect_uri}"
+        . "&state={$state}";
+      
+      // 네이버 인증/동의화면으로 리디렉트
+      return redirect()->to($naver_auth_url);
+      
+    }
+    
+    public function naver(){
+      $client_id = 'tPw7dRu1r7yY89O5gN7n';
+      $client_secret = 'Pgan4lv9l9';
+      $state = $this->request->getGet('state');
+      $code = $this->request->getGet('code');
+      
+      if ($state !== session()->get('naver_state')) {
+        exit('CSRF 실패');
+      }
+      
+      // 토큰 발급
+      $token_url = "https://nid.naver.com/oauth2.0/token"
+        . "?grant_type=authorization_code"
+        . "&client_id={$client_id}"
+        . "&client_secret={$client_secret}"
+        . "&code={$code}"
+        . "&state={$state}";
+
+      // cURL로 토큰 요청
+      $ch = curl_init();
+      curl_setopt($ch, CURLOPT_URL, $token_url);
+      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+      $token_response = curl_exec($ch);
+      curl_close($ch);
+
+      // JSON 파싱
+      $token_data = json_decode($token_response, true);
+      $access_token = $token_data['access_token'] ?? null;
+      
+      // 회원 정보 요청
+      if ($access_token) {
+        $headers = [
+          "Authorization: Bearer {$access_token}"
+        ];
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, "https://openapi.naver.com/v1/nid/me");
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+        $user_info = curl_exec($ch);
+        curl_close($ch);
+        
+        $user_info_arr = json_decode($user_info, true);
+        $user_info_arr['refresh_token'] = $token_data['refresh_token'];
+        $user_info_arr['access_token'] = $token_data['access_token'];
+        
+        //DB 커넥션
+        $db = \Config\Database::connect();
+        
+        // 1. USER_LIST에서 이메일로 조회
+        $id = $user_info_arr['response']['id'];
+        $email = $user_info_arr['response']['email'];
+        $name = $user_info_arr['response']['name'];
+        $phone = $user_info_arr['response']['mobile'];
+        $builder = $db->table('USER_LIST');
+        $user = $builder->where('EMAIL', $email)->get()->getRowArray();
+        
+        
+        if($user){
+          $jwtSecret = env('JWT_SECRET');
+          $kid = env('JWT_KID');
+          
+          if (empty($jwtSecret) || empty($kid)) {
+            return $this->failServerError('환경변수가 누락되었습니다. 관리자에게 문의하세요.');
+          }
+          if (!class_exists('\App\Libraries\JwtLib\JWT')) {
+            return $this->failServerError('JWT 라이브러리를 찾을 수 없습니다.');
+          }
+          
+          $issuedAt = time();
+          $accessExpire = $issuedAt + 60 * 15; // 15분
+          $refreshExpire = $issuedAt + 60 * 60 * 24 * 14; // 14일
+          
+          $accessPayload = [
+            'iat' => $issuedAt,
+            'exp' => $accessExpire,
+            'sub' => $id,
+            'name' => $name ?? '',
+          ];
+          
+          $refreshPayload = [
+            'iat' => $issuedAt,
+            'exp' => $refreshExpire,
+            'sub' => $id,
+          ];
+          
+          try {
+            $accessToken = JWT::encode($accessPayload, $jwtSecret, 'HS256', $kid);
+            $refreshToken = JWT::encode($refreshPayload, $jwtSecret, 'HS256', $kid);
+            
+            // (선택) DB의 리프레시 토큰 값 업데이트
+            $db = \Config\Database::connect();
+            $builder = $db->table('USER_LIST');
+            $builder->where('ID', $id)->update([
+              'REFRESH_TOKEN' => $refreshToken,
+              'NAVER_REFRESH_TOKEN' => $user_info_arr['refresh_token']
+            ]);
+            
+          } catch (\Throwable $e) {
+            return $this->failServerError('JWT 생성 오류: ' . $e->getMessage());
+          }
+          
+          unset($user['PASSWORD']); // 혹시 비번이 있다면 노출 방지
+          
+          $query = http_build_query([
+            "accessToken" => $accessToken,
+            "refreshToken" => $refreshToken,
+            "user" => urlencode(json_encode($user)),
+          ]);
+          
+          //return redirect()->to(base_url("auth/popupClose?$query"));
+          // 개발진행중 풀 URL
+          return redirect()->to(self::FRONTEND_BASE_URL."/auth/popupClose?$query");
+        }else{
+          // 회원이 없으면 회원가입
+          $type = 1;
+          
+          // ID는 auto_increment라면 생략
+          $authData = [
+            'ID' => $id,
+            'NAME' => $name,
+            'PHONE' => $phone,
+            'EMAIL' => $email,
+            'TYPE' => $type,
+            'JOIN' => '1',
+            'NAVER_REFRESH_TOKEN' => $user_info_arr['refresh_token']  ?? null
+          ];
+          
+          $query = http_build_query([
+            "user" => urlencode(json_encode($authData)),
+          ]);
+          
+          
+          //return redirect()->to(base_url("auth/popupClose?$query"));
+          // 개발진행중 풀 URL
+          return redirect()->to(self::FRONTEND_BASE_URL."/auth/popupClose?$query");
+        }
+        
+      } else {
+        // 오류 처리
+      }
+      
+      
+      
+    }
+    
+    public function naverWithdrawal($refreshToken)
+    {
+      
+      // 1. 요청에서 사용자 SEQ 추출 (예: POST or GET)
+      $postData = $this->request->getJSON(true);
+      $seq = $postData['SEQ'];
+      
+      $refreshTokenNaver = $postData['NAVER_REFRESH_TOKEN'];
+      
+      // 2. 사용자 정보 조회 (구글 토큰 포함)
+      $user = $this->userModel->find($seq);
+      
+      if (!$user) {
+        return $this->response->setJSON(['error' => 'User not found'])->setStatusCode(404);
+      }
+      
+      $clientId = 'tPw7dRu1r7yY89O5gN7n';
+      $clientSecret = 'Pgan4lv9l9';
+      
+      // 1. 리프레시 토큰으로 엑세스 토큰 재발급 요청
+      $tokenUrl = "https://nid.naver.com/oauth2.0/token";
+      $tokenParams = [
+        'grant_type' => 'refresh_token',
+        'client_id' => $clientId,
+        'client_secret' => $clientSecret,
+        'refresh_token' => $refreshTokenNaver
+      ];
+      
+      $ch = curl_init();
+      curl_setopt($ch, CURLOPT_URL, $tokenUrl);
+      curl_setopt($ch, CURLOPT_POST, true);
+      curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($tokenParams));
+      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+      $tokenResponse = curl_exec($ch);
+      $tokenError = curl_error($ch);
+      curl_close($ch);
+      
+      if ($tokenError) {
+        return [
+          'success' => false,
+          'message' => "토큰 재발급 오류: " . $tokenError,
+        ];
+      }
+      
+      $tokenData = json_decode($tokenResponse, true);
+      if (empty($tokenData['access_token'])) {
+        return [
+          'success' => false,
+          'message' => '엑세스 토큰 재발급 실패: ' . ($tokenData['error_description'] ?? '알 수 없는 오류'),
+        ];
+      }
+      $accessToken = $tokenData['access_token'];
+      
+      // 2. 엑세스 토큰으로 연결끊기
+      $withdrawUrl = "https://nid.naver.com/oauth2.0/token";
+      $withdrawParams = [
+        'grant_type' => 'delete',
+        'client_id' => $clientId,
+        'client_secret' => $clientSecret,
+        'access_token' => $accessToken,
+        'service_provider' => 'NAVER',
+      ];
+      
+      $ch2 = curl_init();
+      curl_setopt($ch2, CURLOPT_URL, $withdrawUrl);
+      curl_setopt($ch2, CURLOPT_POST, true);
+      curl_setopt($ch2, CURLOPT_POSTFIELDS, http_build_query($withdrawParams));
+      curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
+      $withdrawResponse = curl_exec($ch2);
+      $withdrawError = curl_error($ch2);
+      curl_close($ch2);
+      
+      if ($withdrawError) {
+        return [
+          'success' => false,
+          'message' => "연결해제 요청 오류: " . $withdrawError,
+        ];
+      }
+      
+      $withdrawData = json_decode($withdrawResponse, true);
+      if (isset($withdrawData['result']) && $withdrawData['result'] === 'success') {
+        return [
+          'success' => true,
+          'message' => '네이버 연동 해제 완료',
+        ];
+        
+        // 4. USER_LIST에서 사용자 삭제
+        $this->userModel->delete($seq);
+        
+        // 5. 응답 반환
+        return $this->response->setJSON([
+          'result' => 'User account deleted and Google link revoked'
+        ])->setStatusCode(200);
+        
+      } else {
+        return [
+          'success' => false,
+          'message' => $withdrawData['error'] ?? '연동 해제 실패',
+        ];
+      }
+      
+     
+    }
+    
+    
+    /***********************************
+     * @param $refreshTokenGoogle
+     * @return void
+     **********************************/
+    // 구글 연결 가입정보 연결 해제(회원가입중 페이지 빠져나올경우 리프래시 토큰이 사라짐 고객이 직접 계정에서 해제 필요)
+    protected function revokeGoogleAccess($refreshTokenGoogle)
+    {
+      // 1. 리프레시 토큰으로 엑세스 토큰 발급
+      $client_id = '373780605211-diojebh7mug45urv9rnqdil6n0b1ogge.apps.googleusercontent.com';
+      $client_secret = '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';
+      $refresh_token = $refreshTokenGoogle;
+      
+      $token_url = 'https://oauth2.googleapis.com/token';
+      $params = [
+        'client_id' => $client_id,
+        'client_secret' => $client_secret,
+        'refresh_token' => $refresh_token,
+        'grant_type' => 'refresh_token',
+      ];
+      
+      $ch = curl_init();
+      curl_setopt($ch, CURLOPT_URL, $token_url);
+      curl_setopt($ch, CURLOPT_POST, true);
+      curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
+      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+      
+      $response = curl_exec($ch);
+      curl_close($ch);
+      
+      $data = json_decode($response, true);
+
+      // 2. 새 엑세스 토큰으로 연결 끊기
+      if (!empty($data['access_token'])) {
+        $accessToken = $data['access_token'];
+        $revoke_url = 'https://accounts.google.com/o/oauth2/revoke?token=' . urlencode($accessToken);
+        
+        $ch = curl_init($revoke_url);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_exec($ch);
+        curl_close($ch);
+      } else {
+        // TODO : 엑세스 토큰 발급 실패 처리 필요
+      }
+      
+    }
+    
+    
+    /***********************************
+     * 아이디 체크
+     **********************************/
+    public function  checkId()
+    {
+      $postData = $this->request->getJSON(true);
+      // 필수값 검증
+      if (empty($id) || empty($type)) {
+        return $this->response->setJSON([
+          'status' => 'fail',
+          'message' => '필수 정보가 누락되었습니다.'
+        ])->setStatusCode(400);
+      }
+      
+      $db = \Config\Database::connect();
+      
+      try {
+        // type에 따라 다른 테이블에서 ID 중복 체크
+        if ($type === 'vendor') {
+          // 벤더 회원가입의 경우 VENDOR_LIST 테이블 체크
+          $builder = $db->table('VENDOR_LIST');
+          $existingUser = $builder->where('ID', $id)->get()->getRowArray();
+          
+          if ($existingUser) {
+            return $this->response->setJSON([
+              'status' => 'fail',
+              'message' => '이미 사용 중인 아이디입니다.'
+            ])->setStatusCode(200);
+          }
+        } elseif ($type === 'influencer') {
+          // 인플루언서(일반회원가입)의 경우 USER_LIST 테이블 체크
+          $builder2 = $db->table('USER_LIST');
+          $existingUser = $builder2->where('ID', $id)->get()->getRowArray();
+          
+          if ($existingUser) {
+            return $this->response->setJSON([
+              'status' => 'fail',
+              'message' => '이미 사용 중인 아이디입니다.'
+            ])->setStatusCode(200);
+          }
+        } else {
+          return $this->response->setJSON([
+            'status' => 'fail',
+            'message' => '유효하지 않은 회원 유형입니다.'
+          ])->setStatusCode(400);
+        }
+        
+        // ID가 사용 가능한 경우
+        return $this->response->setJSON([
+          'status' => 'success',
+          'message' => '사용 가능한 아이디입니다.'
+        ])->setStatusCode(200);
+        
+      } catch (\Throwable $e) {
+        return $this->response->setJSON([
+          'status' => 'fail',
+          'message' => 'DB 오류: ' . $e->getMessage()
+        ])->setStatusCode(500);
+      }
+    }
+  }

+ 156 - 0
backend/app/Controllers/DebugController.php

@@ -0,0 +1,156 @@
+<?php
+
+namespace App\Controllers;
+
+use App\Controllers\BaseController;
+use App\Models\VendorModel;
+use App\Models\UserModel;
+use App\Models\VendorInfluencerMappingModel;
+use CodeIgniter\HTTP\ResponseInterface;
+
+class DebugController extends BaseController
+{
+    protected $vendorModel;
+    protected $userModel;
+    protected $vendorInfluencerModel;
+    
+    public function __construct()
+    {
+        $this->vendorModel = new VendorModel();
+        $this->userModel = new UserModel();
+        $this->vendorInfluencerModel = new VendorInfluencerMappingModel();
+    }
+    
+    /**
+     * 외래키 제약조건 디버깅용 메서드
+     */
+    public function debugForeignKey()
+    {
+        try {
+            $mappingSeq = 2;
+            $processedBy = 8;
+            
+            // 1. USER_LIST에서 SEQ 8번 사용자 확인
+            $user = $this->userModel->where('SEQ', $processedBy)->first();
+            $debugInfo = [
+                'user_exists' => !empty($user),
+                'user_data' => $user,
+                'user_count' => $this->userModel->where('SEQ', $processedBy)->countAllResults()
+            ];
+            
+            // 2. VENDOR_INFLUENCER_MAPPING에서 SEQ 2번 레코드 확인
+            $mapping = $this->vendorInfluencerModel->where('SEQ', $mappingSeq)->first();
+            $debugInfo['mapping_exists'] = !empty($mapping);
+            $debugInfo['mapping_data'] = $mapping;
+            
+            // 3. 현재 APPROVED_BY 필드 상태 확인
+            if ($mapping) {
+                $debugInfo['current_approved_by'] = $mapping['APPROVED_BY'];
+                $debugInfo['current_status'] = $mapping['STATUS'];
+            }
+            
+            // 4. 외래키 제약조건 확인
+            $db = \Config\Database::connect();
+            $foreignKeys = $db->query("
+                SELECT 
+                    CONSTRAINT_NAME,
+                    COLUMN_NAME,
+                    REFERENCED_TABLE_NAME,
+                    REFERENCED_COLUMN_NAME
+                FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE 
+                WHERE TABLE_NAME = 'VENDOR_INFLUENCER_MAPPING' 
+                AND TABLE_SCHEMA = DATABASE()
+                AND REFERENCED_TABLE_NAME IS NOT NULL
+            ")->getResultArray();
+            
+            $debugInfo['foreign_keys'] = $foreignKeys;
+            
+            // 5. 실제 업데이트 시도해보기 (트랜잭션 롤백)
+            $db->transStart();
+            
+            try {
+                $updateData = [
+                    'STATUS' => 'APPROVED',
+                    'APPROVED_BY' => $processedBy,
+                    'RESPONSE_MESSAGE' => 'debug test',
+                    'RESPONSE_DATE' => date('Y-m-d H:i:s')
+                ];
+                
+                $result = $this->vendorInfluencerModel->update($mappingSeq, $updateData);
+                $debugInfo['update_attempted'] = true;
+                $debugInfo['update_result'] = $result;
+                $debugInfo['update_error'] = null;
+                
+            } catch (\Exception $e) {
+                $debugInfo['update_attempted'] = true;
+                $debugInfo['update_result'] = false;
+                $debugInfo['update_error'] = $e->getMessage();
+            }
+            
+            // 항상 롤백
+            $db->transRollback();
+            
+            // 6. 다른 사용자 SEQ들 확인
+            $otherUsers = $this->userModel
+                ->select('SEQ, NICK_NAME, EMAIL, IS_ACT, USER_TYPE')
+                ->where('IS_ACT', 'Y')
+                ->orderBy('SEQ')
+                ->findAll(10);
+            
+            $debugInfo['sample_active_users'] = $otherUsers;
+            
+            return $this->response->setJSON([
+                'success' => true,
+                'debug_info' => $debugInfo
+            ]);
+            
+        } catch (\Exception $e) {
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '디버깅 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+    
+    /**
+     * 간단한 업데이트 테스트
+     */
+    public function testSimpleUpdate()
+    {
+        try {
+            $mappingSeq = 2;
+            $processedBy = 8;
+            
+            // 직접 SQL로 업데이트 시도
+            $db = \Config\Database::connect();
+            
+            $sql = "UPDATE VENDOR_INFLUENCER_MAPPING SET APPROVED_BY = ? WHERE SEQ = ?";
+            
+            try {
+                $result = $db->query($sql, [$processedBy, $mappingSeq]);
+                
+                return $this->response->setJSON([
+                    'success' => true,
+                    'message' => '직접 SQL 업데이트 성공',
+                    'affected_rows' => $db->affectedRows()
+                ]);
+                
+            } catch (\Exception $e) {
+                return $this->response->setJSON([
+                    'success' => false,
+                    'message' => '직접 SQL 업데이트 실패',
+                    'error' => $e->getMessage(),
+                    'sql_state' => $db->error()
+                ]);
+            }
+            
+        } catch (\Exception $e) {
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '테스트 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+}

+ 86 - 0
backend/app/Controllers/InfluencerController.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace App\Controllers;
+
+use App\Controllers\BaseController;
+use App\Models\UserModel;
+use App\Models\VendorInfluencerMappingModel;
+use CodeIgniter\HTTP\ResponseInterface;
+
+class InfluencerController extends BaseController
+{
+    protected $userModel;
+    protected $vendorInfluencerModel;
+    
+    public function __construct()
+    {
+        $this->userModel = new UserModel();
+        $this->vendorInfluencerModel = new VendorInfluencerMappingModel();
+    }
+    
+    /**
+     * 인플루언서 프로필 조회
+     */
+    public function getProfile()
+    {
+        try {
+            $request = $this->request->getJSON();
+            $influencerSeq = $request->influencerSeq ?? null;
+            
+            if (!$influencerSeq) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '인플루언서 SEQ가 필요합니다.'
+                ]);
+            }
+            
+            // 인플루언서 프로필 정보 조회
+            $profile = $this->userModel
+                ->select('SEQ, NICK_NAME, NAME, EMAIL, PROFILE_IMAGE, PRIMARY_CATEGORY, 
+                         REGION, DESCRIPTION, SNS_CHANNELS, FOLLOWER_COUNT, ENGAGEMENT_RATE')
+                ->where('SEQ', $influencerSeq)
+                ->where('IS_ACT', 'Y')
+                ->first();
+            
+            if (!$profile) {
+                return $this->response->setStatusCode(404)->setJSON([
+                    'success' => false,
+                    'message' => '인플루언서를 찾을 수 없습니다.'
+                ]);
+            }
+            
+            // 협업 이력 조회
+            $partnerships = $this->vendorInfluencerModel
+                ->select('vim.*, v.COMPANY_NAME as vendorName')
+                ->from('VENDOR_INFLUENCER_MAPPING vim')
+                ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left')
+                ->where('vim.INFLUENCER_SEQ', $influencerSeq)
+                ->where('vim.IS_ACT', 'Y')
+                ->orderBy('vim.REG_DATE', 'DESC')
+                ->findAll();
+            
+            // 협업 건수 계산 (승인된 건수만)
+            $partnershipCount = $this->vendorInfluencerModel
+                ->where('INFLUENCER_SEQ', $influencerSeq)
+                ->where('STATUS', 'APPROVED')
+                ->where('IS_ACT', 'Y')
+                ->countAllResults();
+            
+            return $this->response->setJSON([
+                'success' => true,
+                'data' => [
+                    'profile' => $profile,
+                    'partnerships' => $partnerships,
+                    'partnershipCount' => $partnershipCount
+                ]
+            ]);
+            
+        } catch (\Exception $e) {
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '프로필 조회 중 오류가 발생했습니다.',
+                'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null
+            ]);
+        }
+    }
+} 

+ 198 - 0
backend/app/Controllers/Roulette.php

@@ -0,0 +1,198 @@
+<?php
+
+namespace App\Controllers;
+
+use CodeIgniter\RESTful\ResourceController;
+use App\Libraries\JwtLib\JWT;
+use App\Libraries\JwtLib\Key;
+use App\Models\LoginModel;
+
+class Roulette extends ResourceController
+{
+    public function login()
+    {
+        // JSON 데이터 받기
+        $data = $this->request->getJSON(true);
+
+        $id = $data['id'] ?? null;
+        $password = $data['password'] ?? null;
+        $logintype = $data['logintype'] ?? null;
+
+        if (!$id || !$password) {
+            return $this->fail([
+              'errorCode' => 1000,
+              'message' => '아이디 또는 비밀번호가 누락되었습니다.'
+            ], 400);
+        }
+
+        $loginModel = new LoginModel();
+        $builder = $loginModel->getBuilderFor($logintype); // 모델을 통해 빌더를 가져옴
+
+        
+        $user = $builder->where('ID', $id)->get()->getRowArray();
+
+        if (!$user) {
+            return $this->fail([
+              'errorCode' => 1001,
+              'message' => '존재하지 않는 아이디입니다.'
+            ], 404);
+        }
+
+        // 비밀번호 검증
+        if (!password_verify($password, $user['PASSWORD'])) {
+            return $this->fail([
+              'errorCode' => 1002,
+              'message' => '비밀번호가 틀렸습니다.'
+          ], 401);
+        }
+
+        unset($user['PASSWORD']); // 비밀번호 노출 방지
+
+        // JWT 토큰 생성에 필요한 값 로드
+        $jwtSecret = env('JWT_SECRET');
+        $kid = env('JWT_KID');
+
+        if (empty($jwtSecret) || empty($kid)) {
+            return $this->failServerError('환경변수가 누락되었습니다. 관리자에게 문의하세요.');
+        }
+
+        if (!class_exists('\App\Libraries\JwtLib\JWT')) {
+            return $this->failServerError('JWT 라이브러리를 찾을 수 없습니다.');
+        }
+
+        $issuedAt = time();
+        $accessExpire = $issuedAt + 60 * 15; // 15분
+        //$accessExpire = $issuedAt + 5; // 15분
+        $refreshExpire = $issuedAt + 60 * 60 * 24 * 14; // 14일
+        //$refreshExpire = $issuedAt + 5; // 14일
+
+
+        $accessPayload = [
+            'iat' => $issuedAt,
+            'exp' => $accessExpire,
+            'sub' => $user['ID'],
+            'name' => $user['NAME'] ?? '',
+        ];
+
+        // 리프레시 토큰 existing check
+        $currentRefreshToken = $user['REFRESH_TOKEN'] ?? null;
+        $validRefreshToken = null;
+
+        // 토큰이 **없거나** (빈 값, null), 존재하지만 만료된 경우 모두 새로 발급
+        $needIssueRefresh = !$currentRefreshToken; // null 또는 빈 문자열 등
+
+        if ($currentRefreshToken) {
+            // 기존 리프레시 토큰 유효성 검사
+            try {
+                $key = new Key($jwtSecret, 'HS256');
+                $decoded = JWT::decode($currentRefreshToken, $key);
+
+                if (isset($decoded->exp) && $decoded->exp > time()) {
+                    // 만료되지 않음, 재사용
+                    $validRefreshToken = $currentRefreshToken;
+                    $needIssueRefresh = false;
+                } else {
+                    // 만료됨
+                    $needIssueRefresh = true;
+                }
+            } catch (\Throwable $e) {
+                // 토큰이 변조됐거나 잘못된 경우에도 새로 발급
+                $needIssueRefresh = true;
+            }
+        }
+
+        if ($needIssueRefresh) {
+            // 없거나 만료/무효화 시 새로 발급 및 DB 업데이트
+            $refreshPayload = [
+                'iat' => $issuedAt,
+                'exp' => $refreshExpire,
+                'sub' => $user['ID'],
+            ];
+
+            try {
+                $validRefreshToken = JWT::encode($refreshPayload, $jwtSecret, 'HS256', $kid);
+
+                // ADM_LIST에 리프레시 토큰 값 업데이트 (신규 발급 포함)
+                $builder->where('ID', $user['ID'])->update(['REFRESH_TOKEN' => $validRefreshToken]);
+            } catch (\Throwable $e) {
+                return $this->failServerError('JWT 생성 오류: ' . $e->getMessage());
+            }
+        }
+
+        try {
+            $accessToken = JWT::encode($accessPayload, $jwtSecret, 'HS256', $kid);
+        } catch (\Throwable $e) {
+            return $this->failServerError('JWT 생성 오류: ' . $e->getMessage());
+        }
+
+        return $this->respond([
+            'status' => 'active',
+            'accessToken' => $accessToken,
+            'refreshToken' => $validRefreshToken,
+            'user' => $user,
+        ]);
+    }
+
+    public function refreshToken() 
+    {
+        $data = $this->request->getJSON(true);
+        $refreshToken = $data['refreshToken'] ?? null;
+        if (!$refreshToken) {
+            return $this->fail('리프레시 토큰이 필요합니다.', 400);
+        }
+
+        $jwtSecret = env('JWT_SECRET');
+        $kid = env('JWT_KID');
+
+        try {
+            $key = new Key($jwtSecret, 'HS256');
+            $headers = null;
+            $decoded = JWT::decode($refreshToken, $key, $headers);
+
+            if ($decoded->exp < time()) {
+                return $this->fail('리프레시 토큰이 만료되었습니다.', 401);
+            }
+
+            $userId = $decoded->sub ?? null;
+            if (!$userId) {
+                return $this->fail('유효하지 않은 토큰입니다.', 401);
+            }
+
+            // ADM_LIST에서 해당 유저와 REFRESH_TOKEN 비교
+            $db = \Config\Database::connect();
+            $builder = $db->table('ADM_LIST');
+            $user = $builder->where('ID', $userId)->get()->getRowArray();
+
+            if (!$user) {
+                return $this->fail('사용자를 찾을 수 없습니다.', 404);
+            }
+
+            unset($user['PASSWORD']);
+
+            // DB에 저장된 리프레시 토큰과 요청 리프레시 토큰이 일치하지 않으면 거절
+            if (!isset($user['REFRESH_TOKEN']) || $user['REFRESH_TOKEN'] !== $refreshToken) {
+                return $this->fail('리프레시 토큰 불일치 또는 무효한 요청입니다.', 401);
+            }
+
+            // 일치하면 액세스 토큰 발급
+            $issuedAt = time();
+            $expire = $issuedAt + 60 * 15; // 15분
+            //$expire = $issuedAt + 5; // 15분
+            $accessPayload = [
+                'iat' => $issuedAt,
+                'exp' => $expire,
+                'sub' => $userId,
+                'name' => $user['NAME'] ?? '',
+            ];
+            $accessToken = JWT::encode($accessPayload, $jwtSecret, 'HS256', $kid);
+
+            return $this->respond([
+                'accessToken' => $accessToken,
+                'user' => $user,
+            ]);
+        } catch (\Throwable $e) {
+            return $this->fail('유효하지 않은 리프레시 토큰입니다.', 401);
+        }
+    }
+       
+}

+ 622 - 0
backend/app/Controllers/VendorInfluencerController.php

@@ -0,0 +1,622 @@
+<?php
+
+namespace App\Controllers;
+
+use App\Controllers\BaseController;
+use App\Models\VendorModel;
+use App\Models\UserModel;
+use App\Models\VendorInfluencerMappingModel;
+use CodeIgniter\HTTP\ResponseInterface;
+
+class VendorInfluencerController extends BaseController
+{
+    protected $vendorModel;
+    protected $userModel;
+    protected $vendorInfluencerModel;
+    
+    public function __construct()
+    {
+        $this->vendorModel = new VendorModel();
+        $this->userModel = new UserModel();
+        $this->vendorInfluencerModel = new VendorInfluencerMappingModel();
+    }
+    
+    /**
+     * 벤더사 검색
+     */
+    public function searchVendors()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $keyword = $request->keyword ?? '';
+            $category = $request->category ?? '';
+            $region = $request->region ?? '';
+            $sortBy = $request->sortBy ?? 'latest';
+            $page = intval($request->page ?? 1);
+            $size = intval($request->size ?? 12);
+            $influencerSeq = $request->influencerSeq ?? null;
+            
+            $builder = $this->vendorModel->builder();
+            $builder->where('IS_ACT', 'Y');
+            
+            // 키워드 검색
+            if (!empty($keyword)) {
+                $builder->groupStart()
+                    ->like('COMPANY_NAME', $keyword)
+                    ->orLike('DESCRIPTION', $keyword)
+                    ->orLike('TAGS', $keyword)
+                    ->groupEnd();
+            }
+            
+            // 카테고리 필터
+            if (!empty($category)) {
+                $builder->where('CATEGORY', $category);
+            }
+            
+            // 지역 필터
+            if (!empty($region)) {
+                $builder->where('REGION', $region);
+            }
+            
+            // 정렬
+            switch ($sortBy) {
+                case 'partnership':
+                    $builder->orderBy('PARTNERSHIP_COUNT', 'DESC')
+                        ->orderBy('REG_DATE', 'DESC');
+                    break;
+                case 'name':
+                    $builder->orderBy('COMPANY_NAME', 'ASC');
+                    break;
+                case 'latest':
+                default:
+                    $builder->orderBy('REG_DATE', 'DESC');
+                    break;
+            }
+            
+            // 전체 개수
+            $totalCount = $builder->countAllResults(false);
+            
+            // 페이징
+            $offset = ($page - 1) * $size;
+            $vendors = $builder->limit($size, $offset)->get()->getResultArray();
+            
+            // 파트너십 개수 추가
+            foreach ($vendors as &$vendor) {
+                $partnershipCount = $this->vendorInfluencerModel
+                    ->where('VENDOR_SEQ', $vendor['SEQ'])
+                    ->where('STATUS', 'APPROVED')
+                    ->where('IS_ACT', 'Y')
+                    ->countAllResults();
+                
+                $vendor['PARTNERSHIP_COUNT'] = $partnershipCount;
+                
+                // 인플루언서의 파트너십 상태 확인
+                if ($influencerSeq) {
+                    $partnershipStatus = $this->vendorInfluencerModel
+                        ->where('VENDOR_SEQ', $vendor['SEQ'])
+                        ->where('INFLUENCER_SEQ', $influencerSeq)
+                        ->where('IS_ACT', 'Y')
+                        ->first();
+                    
+                    $vendor['PARTNERSHIP_STATUS'] = $partnershipStatus['STATUS'] ?? null;
+                    $vendor['REQUEST_TYPE'] = $partnershipStatus['REQUEST_TYPE'] ?? null;
+                }
+            }
+            
+            $totalPages = ceil($totalCount / $size);
+            
+            return $this->response->setJSON([
+                'success' => true,
+                'data' => [
+                    'items' => $vendors,
+                    'pagination' => [
+                        'currentPage' => $page,
+                        'totalPages' => $totalPages,
+                        'totalCount' => $totalCount,
+                        'pageSize' => $size,
+                        'hasNext' => $page < $totalPages,
+                        'hasPrev' => $page > 1
+                    ]
+                ]
+            ]);
+            
+        } catch (\Exception $e) {
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '벤더사 검색 중 오류가 발생했습니다.',
+                'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null
+            ]);
+        }
+    }
+    
+    /**
+     * 승인 요청 생성
+     */
+    public function createRequest()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $vendorSeq = $request->vendorSeq ?? null;
+            $influencerSeq = $request->influencerSeq ?? null;
+            $requestType = $request->requestType ?? 'INFLUENCER_REQUEST';
+            $requestMessage = $request->requestMessage ?? '';
+            $requestedBy = $request->requestedBy ?? null;
+            $commissionRate = $request->commissionRate ?? null;
+            $specialConditions = $request->specialConditions ?? '';
+            
+            // 필수 파라미터 검증
+            if (!$vendorSeq || !$influencerSeq || !$requestedBy) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '필수 파라미터가 누락되었습니다.'
+                ]);
+            }
+            
+            // 중복 요청 확인
+            $existingRequest = $this->vendorInfluencerModel
+                ->where('VENDOR_SEQ', $vendorSeq)
+                ->where('INFLUENCER_SEQ', $influencerSeq)
+                ->where('STATUS', 'PENDING')
+                ->where('IS_ACT', 'Y')
+                ->first();
+            
+            if ($existingRequest) {
+                return $this->response->setStatusCode(409)->setJSON([
+                    'success' => false,
+                    'message' => '이미 처리 중인 요청이 있습니다.'
+                ]);
+            }
+            
+            // 요청 생성
+            $data = [
+                'VENDOR_SEQ' => $vendorSeq,
+                'INFLUENCER_SEQ' => $influencerSeq,
+                'REQUEST_TYPE' => $requestType,
+                'STATUS' => 'PENDING',
+                'REQUEST_MESSAGE' => $requestMessage,
+                'REQUESTED_BY' => $requestedBy,
+                'COMMISSION_RATE' => $commissionRate,
+                'SPECIAL_CONDITIONS' => $specialConditions,
+                'EXPIRED_DATE' => date('Y-m-d H:i:s', strtotime('+7 days')),
+                'REG_DATE' => date('Y-m-d H:i:s')
+            ];
+            
+            $insertId = $this->vendorInfluencerModel->insert($data);
+            
+            // 생성된 데이터 조회
+            $createdRequest = $this->vendorInfluencerModel
+                ->select('vim.*, v.COMPANY_NAME as vendorName, u.NICK_NAME as influencerName, req_user.NICK_NAME as requestedByName')
+                ->from('VENDOR_INFLUENCER_MAPPING vim')
+                ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left')
+                ->join('USER_LIST u', 'vim.INFLUENCER_SEQ = u.SEQ', 'left')
+                ->join('USER_LIST req_user', 'vim.REQUESTED_BY = req_user.SEQ', 'left')
+                ->where('vim.SEQ', $insertId)
+                ->get()
+                ->getRowArray();
+            
+            return $this->response->setJSON([
+                'success' => true,
+                'message' => '승인 요청이 성공적으로 생성되었습니다.',
+                'data' => $createdRequest
+            ]);
+            
+        } catch (\Exception $e) {
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '승인 요청 생성 중 오류가 발생했습니다.',
+                'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null
+            ]);
+        }
+    }
+    
+    /**
+     * 매핑 리스트 조회 (벤더사용 승인요청 목록 포함)
+     */
+    public function getList()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $vendorSeq = $request->vendorSeq ?? null;
+            $influencerSeq = $request->influencerSeq ?? null;
+            $status = $request->status ?? null;
+            $requestType = $request->requestType ?? null;
+            $keyword = $request->keyword ?? null;
+            $category = $request->category ?? null;
+            $sortBy = $request->sortBy ?? 'latest';
+            $page = intval($request->page ?? 1);
+            $size = intval($request->size ?? 12);
+            
+            $builder = $this->vendorInfluencerModel->builder();
+            $builder->select('vim.*, v.COMPANY_NAME as vendorName, v.EMAIL as vendorEmail, v.LOGO as vendorLogo,
+                             u.NICK_NAME as influencerNickname, u.NAME as influencerName, 
+                             u.EMAIL as influencerEmail, u.PHONE as influencerPhone,
+                             u.PROFILE_IMAGE as influencerAvatar, u.REGION as influencerRegion,
+                             u.PRIMARY_CATEGORY as influencerCategory, 
+                             u.FOLLOWER_COUNT as followerCount, 
+                             u.AVG_VIEWS as avgViews,
+                             u.ENGAGEMENT_RATE as engagementRate,
+                             u.DESCRIPTION as influencerDescription,
+                             u.SNS_CHANNELS as influencerSnsChannels,
+                             req_user.NICK_NAME as requestedByName, 
+                             app_user.NICK_NAME as approvedByName,
+                             CASE WHEN vim.EXPIRED_DATE < NOW() AND vim.STATUS = "PENDING" 
+                                  THEN "EXPIRED" ELSE vim.STATUS END as displayStatus')
+                    ->from('VENDOR_INFLUENCER_MAPPING vim')
+                    ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left')
+                    ->join('USER_LIST u', 'vim.INFLUENCER_SEQ = u.SEQ', 'left')
+                    ->join('USER_LIST req_user', 'vim.REQUESTED_BY = req_user.SEQ', 'left')
+                    ->join('USER_LIST app_user', 'vim.APPROVED_BY = app_user.SEQ', 'left')
+                    ->where('vim.IS_ACT', 'Y');
+            
+            // 필터 적용
+            if ($vendorSeq) {
+                $builder->where('vim.VENDOR_SEQ', $vendorSeq);
+            }
+            
+            if ($influencerSeq) {
+                $builder->where('vim.INFLUENCER_SEQ', $influencerSeq);
+            }
+            
+            if ($status) {
+                $builder->where('vim.STATUS', $status);
+            }
+            
+            if ($requestType) {
+                $builder->where('vim.REQUEST_TYPE', $requestType);
+            }
+            
+            // 키워드 검색 (인플루언서명)
+            if (!empty($keyword)) {
+                $builder->groupStart()
+                    ->like('u.NICK_NAME', $keyword)
+                    ->orLike('u.NAME', $keyword)
+                    ->groupEnd();
+            }
+            
+            // 카테고리 필터 (인플루언서 카테고리)
+            if (!empty($category)) {
+                $builder->where('u.PRIMARY_CATEGORY', $category);
+            }
+            
+            // 전체 개수
+            $totalCount = $builder->countAllResults(false);
+            
+            // 정렬
+            switch ($sortBy) {
+                case 'oldest':
+                    $builder->orderBy('vim.REG_DATE', 'ASC');
+                    break;
+                case 'expiring':
+                    $builder->orderBy('vim.EXPIRED_DATE', 'ASC');
+                    break;
+                case 'latest':
+                default:
+                    $builder->orderBy('vim.REG_DATE', 'DESC');
+                    break;
+            }
+            
+            // 페이징
+            $offset = ($page - 1) * $size;
+            $items = $builder->limit($size, $offset)->get()->getResultArray();
+            
+            $totalPages = ceil($totalCount / $size);
+            
+            // 통계 데이터 계산 (벤더사용)
+            $stats = [];
+            if ($vendorSeq) {
+                $statsBuilder = $this->vendorInfluencerModel->builder();
+                $stats = [
+                    'pending' => $statsBuilder->where('VENDOR_SEQ', $vendorSeq)->where('STATUS', 'PENDING')->where('IS_ACT', 'Y')->countAllResults(),
+                    'approved' => $statsBuilder->resetQuery()->where('VENDOR_SEQ', $vendorSeq)->where('STATUS', 'APPROVED')->where('IS_ACT', 'Y')->countAllResults(),
+                    'rejected' => $statsBuilder->resetQuery()->where('VENDOR_SEQ', $vendorSeq)->where('STATUS', 'REJECTED')->where('IS_ACT', 'Y')->countAllResults(),
+                    'total' => $totalCount
+                ];
+            }
+            
+            return $this->response->setJSON([
+                'success' => true,
+                'data' => [
+                    'items' => $items,
+                    'pagination' => [
+                        'currentPage' => $page,
+                        'totalPages' => $totalPages,
+                        'totalCount' => $totalCount,
+                        'pageSize' => $size,
+                        'hasNext' => $page < $totalPages,
+                        'hasPrev' => $page > 1
+                    ],
+                    'stats' => $stats
+                ]
+            ]);
+            
+        } catch (\Exception $e) {
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '리스트 조회 중 오류가 발생했습니다.',
+                'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null
+            ]);
+        }
+    }
+    
+    /**
+     * 요청 취소
+     */
+    public function cancelRequest()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $mappingSeq = $request->mappingSeq ?? null;
+            $cancelledBy = $request->cancelledBy ?? null;
+            $cancelReason = $request->cancelReason ?? '요청자에 의해 취소됨';
+            
+            // 필수 파라미터 검증
+            if (!$mappingSeq || !$cancelledBy) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '필수 파라미터가 누락되었습니다.'
+                ]);
+            }
+            
+            // 기존 요청 확인
+            $existingMapping = $this->vendorInfluencerModel
+                ->where('SEQ', $mappingSeq)
+                ->where('STATUS', 'PENDING')
+                ->where('IS_ACT', 'Y')
+                ->first();
+            
+            if (!$existingMapping) {
+                return $this->response->setStatusCode(404)->setJSON([
+                    'success' => false,
+                    'message' => '취소할 수 있는 요청을 찾을 수 없습니다.'
+                ]);
+            }
+            
+            // 권한 확인
+            if ($existingMapping['REQUESTED_BY'] != $cancelledBy) {
+                return $this->response->setStatusCode(403)->setJSON([
+                    'success' => false,
+                    'message' => '요청을 취소할 권한이 없습니다.'
+                ]);
+            }
+            
+            // 취소 처리
+            $updateData = [
+                'STATUS' => 'CANCELLED',
+                'RESPONSE_MESSAGE' => $cancelReason,
+                'RESPONSE_DATE' => date('Y-m-d H:i:s'),
+                'APPROVED_BY' => $cancelledBy
+            ];
+            
+            $this->vendorInfluencerModel->update($mappingSeq, $updateData);
+            
+            // 업데이트된 데이터 조회
+            $updatedMapping = $this->vendorInfluencerModel
+                ->select('vim.*, v.COMPANY_NAME as vendorName, u.NICK_NAME as influencerName, req_user.NICK_NAME as requestedByName')
+                ->from('VENDOR_INFLUENCER_MAPPING vim')
+                ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left')
+                ->join('USER_LIST u', 'vim.INFLUENCER_SEQ = u.SEQ', 'left')
+                ->join('USER_LIST req_user', 'vim.REQUESTED_BY = req_user.SEQ', 'left')
+                ->where('vim.SEQ', $mappingSeq)
+                ->get()
+                ->getRowArray();
+            
+            return $this->response->setJSON([
+                'success' => true,
+                'message' => '요청이 취소되었습니다.',
+                'data' => $updatedMapping
+            ]);
+            
+        } catch (\Exception $e) {
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '요청 취소 중 오류가 발생했습니다.',
+                'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null
+            ]);
+        }
+    }
+    
+    /**
+     * 요청 승인/거부 처리
+     */
+    public function approveRequest()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $mappingSeq = $request->mappingSeq ?? null;
+            $action = $request->action ?? 'APPROVE'; // 'APPROVE' or 'REJECT'
+            $processedBy = $request->processedBy ?? $request->approvedBy ?? null; // 기존 코드 호환성
+            $responseMessage = $request->responseMessage ?? '';
+            $commissionRate = $request->commissionRate ?? null;
+            
+            if (!$mappingSeq || !$processedBy) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '필수 파라미터가 누락되었습니다.'
+                ]);
+            }
+            
+            // 처리자 벤더사 존재 확인
+            $processingVendor = $this->vendorModel
+              ->where('SEQ', $processedBy)
+              ->first(); // IS_ACT 조건 제거해서 벤더사 존재 여부만 확인
+            
+            if (!$processingVendor) {
+              return $this->response->setStatusCode(400)->setJSON([
+                'success' => false,
+                'message' => "처리자 SEQ {$processedBy}가 VENDOR_LIST 테이블에 존재하지 않습니다."
+              ]);
+            }
+            
+            // 벤더사 활성 상태 확인
+            if ($processingVendor['IS_ACT'] !== 'Y') {
+              return $this->response->setStatusCode(400)->setJSON([
+                'success' => false,
+                'message' => "처리자 SEQ {$processedBy}는 비활성 상태입니다. (IS_ACT: {$processingVendor['IS_ACT']})"
+              ]);
+            }
+            
+            // 기존 요청 확인
+            $existingMapping = $this->vendorInfluencerModel
+                ->where('SEQ', $mappingSeq)
+                ->where('STATUS', 'PENDING')
+                ->where('IS_ACT', 'Y')
+                ->first();
+            
+            if (!$existingMapping) {
+                return $this->response->setStatusCode(404)->setJSON([
+                    'success' => false,
+                    'message' => '처리할 수 있는 요청을 찾을 수 없습니다.'
+                ]);
+            }
+            
+            // 벤더사 권한 확인 (선택적)
+            // 벤더사만 자신의 요청을 처리할 수 있도록 하려면 주석 해제
+            // if ($existingMapping['VENDOR_SEQ'] !== $processingUser['SEQ']) {
+            //     return $this->response->setStatusCode(403)->setJSON([
+            //         'success' => false,
+            //         'message' => '해당 요청을 처리할 권한이 없습니다.'
+            //     ]);
+            // }
+            
+            $status = $action === 'APPROVE' ? 'APPROVED' : 'REJECTED';
+            
+            $updateData = [
+                'STATUS' => $status,
+                'APPROVED_BY' => $processedBy,
+                'RESPONSE_MESSAGE' => $responseMessage,
+                'RESPONSE_DATE' => date('Y-m-d H:i:s')
+            ];
+            
+            if ($commissionRate !== null && $action === 'APPROVE') {
+                $updateData['COMMISSION_RATE'] = $commissionRate;
+            }
+            
+            // 단계별 업데이트로 외래키 제약조건 문제 우회
+            try {
+                // 1단계: APPROVED_BY 없이 먼저 업데이트
+                $firstUpdateData = [
+                    'STATUS' => $status,
+                    'RESPONSE_MESSAGE' => $responseMessage,
+                    'RESPONSE_DATE' => date('Y-m-d H:i:s')
+                ];
+                
+                if ($commissionRate !== null && $action === 'APPROVE') {
+                    $firstUpdateData['COMMISSION_RATE'] = $commissionRate;
+                }
+                
+                log_message('debug', "1단계 업데이트 시도: " . json_encode($firstUpdateData));
+                $result1 = $this->vendorInfluencerModel->update($mappingSeq, $firstUpdateData);
+                log_message('debug', "1단계 업데이트 결과: " . ($result1 ? 'SUCCESS' : 'FAILED'));
+                
+                // 2단계: APPROVED_BY만 별도로 업데이트
+                $secondUpdateData = [
+                    'APPROVED_BY' => $processedBy
+                ];
+                
+                log_message('debug', "2단계 업데이트 시도: " . json_encode($secondUpdateData));
+                $result2 = $this->vendorInfluencerModel->update($mappingSeq, $secondUpdateData);
+                log_message('debug', "2단계 업데이트 결과: " . ($result2 ? 'SUCCESS' : 'FAILED'));
+                
+                if (!$result1 || !$result2) {
+                    throw new \Exception("단계별 업데이트 실패: 1단계={$result1}, 2단계={$result2}");
+                }
+                
+            } catch (\Exception $updateException) {
+                log_message('error', "단계별 업데이트 실패: " . $updateException->getMessage());
+                
+                // 외래키 제약조건 오류인 경우 더 상세한 정보 제공
+                if (strpos($updateException->getMessage(), 'foreign key constraint') !== false) {
+                    
+                    // 추가 디버깅: 직접 SQL로 사용자 존재 확인
+                    $db = \Config\Database::connect();
+                    $userCheck = $db->query("SELECT SEQ, NICK_NAME, EMAIL, IS_ACT FROM USER_LIST WHERE SEQ = ?", [$processedBy])->getRowArray();
+                    
+                    // 추가 디버깅: 현재 매핑 레코드 상태 확인
+                    $mappingCheck = $db->query("SELECT SEQ, STATUS, APPROVED_BY FROM VENDOR_INFLUENCER_MAPPING WHERE SEQ = ?", [$mappingSeq])->getRowArray();
+                    
+                    return $this->response->setStatusCode(500)->setJSON([
+                        'success' => false,
+                        'message' => '외래키 제약조건 오류가 발생했습니다.',
+                        'error' => $updateException->getMessage(),
+                        'debug_info' => [
+                            'processedBy' => $processedBy,
+                            'processingVendor' => $processingVendor,
+                            'existingMapping' => $existingMapping,
+                            'userCheck' => $userCheck,
+                            'mappingCheck' => $mappingCheck,
+                            'updateData' => $updateData
+                        ]
+                    ]);
+                }
+                
+                throw $updateException; // 다른 예외는 기존 처리 방식 유지
+            }
+            
+            $actionText = $action === 'APPROVE' ? '승인' : '거부';
+            return $this->response->setJSON([
+                'success' => true,
+                'message' => "요청이 {$actionText}되었습니다.",
+                'data' => $updateData
+            ]);
+            
+        } catch (\Exception $e) {
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '요청 처리 중 오류가 발생했습니다.',
+                'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null
+            ]);
+        }
+    }
+    
+    /**
+     * 상세 조회
+     */
+    public function getDetail()
+    {
+        try {
+            $request = $this->request->getJSON();
+            $mappingSeq = $request->mappingSeq ?? null;
+            
+            if (!$mappingSeq) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '매핑 SEQ가 필요합니다.'
+                ]);
+            }
+            
+            $detail = $this->vendorInfluencerModel
+                ->select('vim.*, v.*, u.NICK_NAME as influencerName, u.EMAIL as influencerEmail')
+                ->from('VENDOR_INFLUENCER_MAPPING vim')
+                ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left')
+                ->join('USER_LIST u', 'vim.INFLUENCER_SEQ = u.SEQ', 'left')
+                ->where('vim.SEQ', $mappingSeq)
+                ->where('vim.IS_ACT', 'Y')
+                ->get()
+                ->getRowArray();
+            
+            if (!$detail) {
+                return $this->response->setStatusCode(404)->setJSON([
+                    'success' => false,
+                    'message' => '요청을 찾을 수 없습니다.'
+                ]);
+            }
+            
+            return $this->response->setJSON([
+                'success' => true,
+                'data' => $detail
+            ]);
+            
+        } catch (\Exception $e) {
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '상세 조회 중 오류가 발생했습니다.',
+                'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null
+            ]);
+        }
+    }
+}

+ 223 - 0
backend/app/Models/UserModel.php

@@ -0,0 +1,223 @@
+<?php
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class UserModel extends Model
+{
+    protected $table = 'USER_LIST';
+    protected $primaryKey = 'SEQ';
+    protected $useAutoIncrement = true;
+    protected $returnType = 'array';
+    protected $useSoftDeletes = false;
+    
+    protected $allowedFields = [
+        'ID',
+        'PASSWORD',
+        'NICK_NAME',
+        'EMAIL',
+        'PHONE',
+        'MEMBER_TYPE',
+        'STATUS',
+        'LAST_LOGIN_DATE',
+        'IS_ACT',
+        'REG_DATE',
+        'MOD_DATE',
+        // 인플루언서 관련 필드들
+        'INFLUENCER_TYPE',
+        'PRIMARY_CATEGORY',
+        'FOLLOWER_COUNT',
+        'AVG_VIEWS',
+        'PROFILE_IMAGE',
+        'BIO',
+        'INSTAGRAM_URL',
+        'YOUTUBE_URL',
+        'TIKTOK_URL',
+        'BLOG_URL',
+        'PREFERRED_REGION',
+        'MIN_COMMISSION_RATE',
+        'VERIFICATION_STATUS',
+        'VERIFIED_DATE'
+    ];
+    
+    protected $useTimestamps = true;
+    protected $createdField = 'REG_DATE';
+    protected $updatedField = 'MOD_DATE';
+    
+    protected $validationRules = [
+        'ID' => 'required|max_length[50]|is_unique[USER_LIST.ID,SEQ,{SEQ}]',
+        'PASSWORD' => 'required|min_length[8]',
+        'NICK_NAME' => 'required|max_length[100]',
+        'EMAIL' => 'required|valid_email|is_unique[USER_LIST.EMAIL,SEQ,{SEQ}]',
+        'PHONE' => 'permit_empty|max_length[20]',
+        'MEMBER_TYPE' => 'required|in_list[ADMIN,INFLUENCER,VENDOR]',
+        'STATUS' => 'required|in_list[ACTIVE,INACTIVE,SUSPENDED,PENDING]',
+        'IS_ACT' => 'required|in_list[Y,N]',
+        'INFLUENCER_TYPE' => 'permit_empty|in_list[MACRO,MICRO,NANO,MEGA]',
+        'PRIMARY_CATEGORY' => 'permit_empty|in_list[FASHION_BEAUTY,FOOD_HEALTH,LIFESTYLE,TECH_ELECTRONICS,SPORTS_LEISURE,CULTURE_ENTERTAINMENT]',
+        'FOLLOWER_COUNT' => 'permit_empty|integer|greater_than_equal_to[0]',
+        'AVG_VIEWS' => 'permit_empty|integer|greater_than_equal_to[0]',
+        'PREFERRED_REGION' => 'permit_empty|in_list[SEOUL,GYEONGGI,INCHEON,BUSAN,DAEGU,DAEJEON,GWANGJU,ULSAN,OTHER]',
+        'MIN_COMMISSION_RATE' => 'permit_empty|decimal|greater_than_equal_to[0]|less_than_equal_to[100]',
+        'VERIFICATION_STATUS' => 'permit_empty|in_list[UNVERIFIED,PENDING,VERIFIED,REJECTED]'
+    ];
+    
+    protected $validationMessages = [
+        'ID' => [
+            'required' => '아이디는 필수입니다.',
+            'max_length' => '아이디는 50자를 초과할 수 없습니다.',
+            'is_unique' => '이미 사용 중인 아이디입니다.'
+        ],
+        'PASSWORD' => [
+            'required' => '비밀번호는 필수입니다.',
+            'min_length' => '비밀번호는 최소 8자 이상이어야 합니다.'
+        ],
+        'NICK_NAME' => [
+            'required' => '닉네임은 필수입니다.',
+            'max_length' => '닉네임은 100자를 초과할 수 없습니다.'
+        ],
+        'EMAIL' => [
+            'required' => '이메일은 필수입니다.',
+            'valid_email' => '유효한 이메일 형식이 아닙니다.',
+            'is_unique' => '이미 사용 중인 이메일입니다.'
+        ],
+        'MEMBER_TYPE' => [
+            'required' => '회원 유형은 필수입니다.',
+            'in_list' => '유효하지 않은 회원 유형입니다.'
+        ],
+        'STATUS' => [
+            'required' => '상태는 필수입니다.',
+            'in_list' => '유효하지 않은 상태입니다.'
+        ],
+        'IS_ACT' => [
+            'required' => '활성 상태는 필수입니다.',
+            'in_list' => '활성 상태는 Y 또는 N이어야 합니다.'
+        ]
+    ];
+    
+    protected $skipValidation = false;
+    protected $cleanValidationRules = true;
+    
+    /**
+     * 인플루언서 목록 조회
+     */
+    public function getInfluencers($filters = [], $page = 1, $perPage = 12)
+    {
+        $builder = $this->where('MEMBER_TYPE', 'INFLUENCER')
+                        ->where('IS_ACT', 'Y')
+                        ->where('STATUS', 'ACTIVE');
+        
+        // 키워드 검색
+        if (!empty($filters['keyword'])) {
+            $builder->groupStart()
+                   ->like('NICK_NAME', $filters['keyword'])
+                   ->orLike('ID', $filters['keyword'])
+                   ->groupEnd();
+        }
+        
+        // 카테고리 필터
+        if (!empty($filters['category'])) {
+            $builder->where('PRIMARY_CATEGORY', $filters['category']);
+        }
+        
+        // 인플루언서 타입 필터
+        if (!empty($filters['influencer_type'])) {
+            $builder->where('INFLUENCER_TYPE', $filters['influencer_type']);
+        }
+        
+        // 팔로워 수 범위
+        if (!empty($filters['follower_min'])) {
+            $builder->where('FOLLOWER_COUNT >=', $filters['follower_min']);
+        }
+        if (!empty($filters['follower_max'])) {
+            $builder->where('FOLLOWER_COUNT <=', $filters['follower_max']);
+        }
+        
+        // 페이징
+        $offset = ($page - 1) * $perPage;
+        return $builder->limit($perPage, $offset)->findAll();
+    }
+    
+    /**
+     * 인플루언서 검색 결과 총 개수
+     */
+    public function countInfluencers($filters = [])
+    {
+        $builder = $this->where('MEMBER_TYPE', 'INFLUENCER')
+                        ->where('IS_ACT', 'Y')
+                        ->where('STATUS', 'ACTIVE');
+        
+        // 키워드 검색
+        if (!empty($filters['keyword'])) {
+            $builder->groupStart()
+                   ->like('NICK_NAME', $filters['keyword'])
+                   ->orLike('ID', $filters['keyword'])
+                   ->groupEnd();
+        }
+        
+        // 카테고리 필터
+        if (!empty($filters['category'])) {
+            $builder->where('PRIMARY_CATEGORY', $filters['category']);
+        }
+        
+        // 인플루언서 타입 필터
+        if (!empty($filters['influencer_type'])) {
+            $builder->where('INFLUENCER_TYPE', $filters['influencer_type']);
+        }
+        
+        // 팔로워 수 범위
+        if (!empty($filters['follower_min'])) {
+            $builder->where('FOLLOWER_COUNT >=', $filters['follower_min']);
+        }
+        if (!empty($filters['follower_max'])) {
+            $builder->where('FOLLOWER_COUNT <=', $filters['follower_max']);
+        }
+        
+        return $builder->countAllResults();
+    }
+    
+    /**
+     * 인플루언서 타입별 통계
+     */
+    public function getInfluencerTypeStats()
+    {
+        return $this->select('INFLUENCER_TYPE, COUNT(*) as count')
+                   ->where('MEMBER_TYPE', 'INFLUENCER')
+                   ->where('IS_ACT', 'Y')
+                   ->where('STATUS', 'ACTIVE')
+                   ->groupBy('INFLUENCER_TYPE')
+                   ->findAll();
+    }
+    
+    /**
+     * 카테고리별 인플루언서 통계
+     */
+    public function getInfluencerCategoryStats()
+    {
+        return $this->select('PRIMARY_CATEGORY, COUNT(*) as count')
+                   ->where('MEMBER_TYPE', 'INFLUENCER')
+                   ->where('IS_ACT', 'Y')
+                   ->where('STATUS', 'ACTIVE')
+                   ->groupBy('PRIMARY_CATEGORY')
+                   ->findAll();
+    }
+    
+    /**
+     * 사용자 로그인
+     */
+    public function authenticate($id, $password)
+    {
+        $user = $this->where('ID', $id)
+                    ->where('IS_ACT', 'Y')
+                    ->first();
+        
+        if ($user && password_verify($password, $user['PASSWORD'])) {
+            // 로그인 날짜 업데이트
+            $this->update($user['SEQ'], ['LAST_LOGIN_DATE' => date('Y-m-d H:i:s')]);
+            return $user;
+        }
+        
+        return false;
+    }
+}

+ 147 - 0
backend/app/Models/VendorInfluencerMappingModel.php

@@ -0,0 +1,147 @@
+<?php
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class VendorInfluencerMappingModel extends Model
+{
+    protected $table = 'VENDOR_INFLUENCER_MAPPING';
+    protected $primaryKey = 'SEQ';
+    protected $useAutoIncrement = true;
+    protected $returnType = 'array';
+    protected $useSoftDeletes = false;
+    
+    protected $allowedFields = [
+        'VENDOR_SEQ',
+        'INFLUENCER_SEQ', 
+        'REQUEST_TYPE',
+        'STATUS',
+        'REQUEST_MESSAGE',
+        'RESPONSE_MESSAGE',
+        'REQUESTED_BY',
+        'APPROVED_BY',
+        'COMMISSION_RATE',
+        'SPECIAL_CONDITIONS',
+        'EXPIRED_DATE',
+        'REQUEST_DATE',
+        'RESPONSE_DATE',
+        'REG_DATE',
+        'MOD_DATE',
+        'IS_ACT'
+    ];
+    
+    protected $useTimestamps = true;
+    protected $createdField = 'REG_DATE';
+    protected $updatedField = 'MOD_DATE';
+    
+    protected $validationRules = [
+        'VENDOR_SEQ' => 'required|integer',
+        'INFLUENCER_SEQ' => 'required|integer',
+        'REQUEST_TYPE' => 'required|in_list[INFLUENCER_REQUEST,VENDOR_REQUEST]',
+        'STATUS' => 'required|in_list[PENDING,APPROVED,REJECTED,CANCELLED,EXPIRED]',
+        'REQUESTED_BY' => 'required|integer',
+    ];
+    
+    protected $validationMessages = [
+        'VENDOR_SEQ' => [
+            'required' => '벤더 SEQ는 필수입니다.',
+            'integer' => '벤더 SEQ는 숫자여야 합니다.'
+        ],
+        'INFLUENCER_SEQ' => [
+            'required' => '인플루언서 SEQ는 필수입니다.',
+            'integer' => '인플루언서 SEQ는 숫자여야 합니다.'
+        ],
+        'REQUEST_TYPE' => [
+            'required' => '요청 타입은 필수입니다.',
+            'in_list' => '유효하지 않은 요청 타입입니다.'
+        ],
+        'STATUS' => [
+            'required' => '상태는 필수입니다.',
+            'in_list' => '유효하지 않은 상태입니다.'
+        ],
+        'REQUESTED_BY' => [
+            'required' => '요청자는 필수입니다.',
+            'integer' => '요청자는 숫자여야 합니다.'
+        ]
+    ];
+    
+    protected $skipValidation = false;
+    protected $cleanValidationRules = true;
+    
+    /**
+     * 만료된 요청들을 처리
+     */
+    public function processExpiredRequests()
+    {
+        return $this->where('STATUS', 'PENDING')
+                   ->where('EXPIRED_DATE <', date('Y-m-d H:i:s'))
+                   ->where('IS_ACT', 'Y')
+                   ->set([
+                       'STATUS' => 'EXPIRED',
+                       'MOD_DATE' => date('Y-m-d H:i:s')
+                   ])
+                   ->update();
+    }
+    
+    /**
+     * 특정 벤더-인플루언서 조합의 활성 요청 확인
+     */
+    public function getActiveRequest($vendorSeq, $influencerSeq)
+    {
+        return $this->where('VENDOR_SEQ', $vendorSeq)
+                   ->where('INFLUENCER_SEQ', $influencerSeq)
+                   ->where('STATUS', 'PENDING')
+                   ->where('IS_ACT', 'Y')
+                   ->first();
+    }
+    
+    /**
+     * 사용자의 요청 목록 조회
+     */
+    public function getUserRequests($userSeq, $asInfluencer = true, $status = null)
+    {
+        $field = $asInfluencer ? 'INFLUENCER_SEQ' : 'VENDOR_SEQ';
+        
+        $builder = $this->where($field, $userSeq)
+                        ->where('IS_ACT', 'Y');
+        
+        if ($status) {
+            $builder->where('STATUS', $status);
+        }
+        
+        return $builder->findAll();
+    }
+    
+    /**
+     * 벤더사별 승인요청 통계
+     */
+    public function getVendorRequestStats($vendorSeq)
+    {
+        $builder = $this->where('VENDOR_SEQ', $vendorSeq)
+                        ->where('IS_ACT', 'Y');
+        
+        return [
+            'pending' => $builder->where('STATUS', 'PENDING')->countAllResults(false),
+            'approved' => $builder->where('STATUS', 'APPROVED')->countAllResults(false),
+            'rejected' => $builder->where('STATUS', 'REJECTED')->countAllResults(false),
+            'total' => $builder->countAllResults()
+        ];
+    }
+    
+    /**
+     * 인플루언서별 요청 통계
+     */
+    public function getInfluencerRequestStats($influencerSeq)
+    {
+        $builder = $this->where('INFLUENCER_SEQ', $influencerSeq)
+                        ->where('IS_ACT', 'Y');
+        
+        return [
+            'pending' => $builder->where('STATUS', 'PENDING')->countAllResults(false),
+            'approved' => $builder->where('STATUS', 'APPROVED')->countAllResults(false),
+            'rejected' => $builder->where('STATUS', 'REJECTED')->countAllResults(false),
+            'total' => $builder->countAllResults()
+        ];
+    }
+}

+ 189 - 0
backend/app/Models/VendorModel.php

@@ -0,0 +1,189 @@
+<?php
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class VendorModel extends Model
+{
+    protected $table = 'VENDOR_LIST';
+    protected $primaryKey = 'SEQ';
+    protected $useAutoIncrement = true;
+    protected $returnType = 'array';
+    protected $useSoftDeletes = false;
+    
+    protected $allowedFields = [
+        'COMPANY_NAME',
+        'EMAIL', 
+        'CATEGORY',
+        'REGION',
+        'DESCRIPTION',
+        'LOGO',
+        'TAGS',
+        'APPROVAL_STATUS',
+        'APPROVED_DATE',
+        'IS_ACT',
+        'REG_DATE',
+        'MOD_DATE',
+        'LAST_LOGIN_DATE',
+        'PASSWORD',
+        'PHONE',
+        'ADDRESS',
+        'BUSINESS_NUMBER',
+        'CEO_NAME',
+        'ESTABLISHMENT_DATE'
+    ];
+    
+    protected $useTimestamps = true;
+    protected $createdField = 'REG_DATE';
+    protected $updatedField = 'MOD_DATE';
+    
+    protected $validationRules = [
+        'COMPANY_NAME' => 'required|max_length[255]',
+        'EMAIL' => 'required|valid_email|is_unique[VENDOR_LIST.EMAIL,SEQ,{SEQ}]',
+        'CATEGORY' => 'permit_empty|in_list[FASHION_BEAUTY,FOOD_HEALTH,LIFESTYLE,TECH_ELECTRONICS,SPORTS_LEISURE,CULTURE_ENTERTAINMENT]',
+        'REGION' => 'permit_empty|in_list[SEOUL,GYEONGGI,INCHEON,BUSAN,DAEGU,DAEJEON,GWANGJU,ULSAN,OTHER]',
+        'APPROVAL_STATUS' => 'permit_empty|in_list[PENDING,APPROVED,REJECTED]',
+        'IS_ACT' => 'required|in_list[Y,N]',
+        'PHONE' => 'permit_empty|max_length[20]',
+        'BUSINESS_NUMBER' => 'permit_empty|max_length[20]'
+    ];
+    
+    protected $validationMessages = [
+        'COMPANY_NAME' => [
+            'required' => '회사명은 필수입니다.',
+            'max_length' => '회사명은 255자를 초과할 수 없습니다.'
+        ],
+        'EMAIL' => [
+            'required' => '이메일은 필수입니다.',
+            'valid_email' => '유효한 이메일 형식이 아닙니다.',
+            'is_unique' => '이미 등록된 이메일입니다.'
+        ],
+        'CATEGORY' => [
+            'in_list' => '유효하지 않은 카테고리입니다.'
+        ],
+        'REGION' => [
+            'in_list' => '유효하지 않은 지역입니다.'
+        ],
+        'APPROVAL_STATUS' => [
+            'in_list' => '유효하지 않은 승인 상태입니다.'
+        ],
+        'IS_ACT' => [
+            'required' => '활성 상태는 필수입니다.',
+            'in_list' => '활성 상태는 Y 또는 N이어야 합니다.'
+        ]
+    ];
+    
+    protected $skipValidation = false;
+    protected $cleanValidationRules = true;
+    
+    /**
+     * 벤더사 검색
+     */
+    public function searchVendors($filters = [], $page = 1, $perPage = 12)
+    {
+        $builder = $this->builder();
+        $builder->where('IS_ACT', 'Y');
+        
+        // 키워드 검색
+        if (!empty($filters['keyword'])) {
+            $builder->groupStart()
+                   ->like('COMPANY_NAME', $filters['keyword'])
+                   ->orLike('DESCRIPTION', $filters['keyword'])
+                   ->orLike('TAGS', $filters['keyword'])
+                   ->groupEnd();
+        }
+        
+        // 카테고리 필터
+        if (!empty($filters['category'])) {
+            $builder->where('CATEGORY', $filters['category']);
+        }
+        
+        // 지역 필터
+        if (!empty($filters['region'])) {
+            $builder->where('REGION', $filters['region']);
+        }
+        
+        // 정렬
+        switch ($filters['sortBy'] ?? 'latest') {
+            case 'partnership':
+                $builder->orderBy('PARTNERSHIP_COUNT', 'DESC')
+                       ->orderBy('REG_DATE', 'DESC');
+                break;
+            case 'name':
+                $builder->orderBy('COMPANY_NAME', 'ASC');
+                break;
+            case 'latest':
+            default:
+                $builder->orderBy('REG_DATE', 'DESC');
+                break;
+        }
+        
+        // 페이징
+        $offset = ($page - 1) * $perPage;
+        return $builder->limit($perPage, $offset)->findAll();
+    }
+    
+    /**
+     * 검색 결과 총 개수
+     */
+    public function countSearchResults($filters = [])
+    {
+        $builder = $this->builder();
+        $builder->where('IS_ACT', 'Y');
+        
+        // 키워드 검색
+        if (!empty($filters['keyword'])) {
+            $builder->groupStart()
+                   ->like('COMPANY_NAME', $filters['keyword'])
+                   ->orLike('DESCRIPTION', $filters['keyword'])
+                   ->orLike('TAGS', $filters['keyword'])
+                   ->groupEnd();
+        }
+        
+        // 카테고리 필터
+        if (!empty($filters['category'])) {
+            $builder->where('CATEGORY', $filters['category']);
+        }
+        
+        // 지역 필터
+        if (!empty($filters['region'])) {
+            $builder->where('REGION', $filters['region']);
+        }
+        
+        return $builder->countAllResults();
+    }
+    
+    /**
+     * 활성 벤더사 목록
+     */
+    public function getActiveVendors()
+    {
+        return $this->where('IS_ACT', 'Y')
+                   ->where('APPROVAL_STATUS', 'APPROVED')
+                   ->orderBy('REG_DATE', 'DESC')
+                   ->findAll();
+    }
+    
+    /**
+     * 카테고리별 벤더사 통계
+     */
+    public function getCategoryStats()
+    {
+        return $this->select('CATEGORY, COUNT(*) as count')
+                   ->where('IS_ACT', 'Y')
+                   ->groupBy('CATEGORY')
+                   ->findAll();
+    }
+    
+    /**
+     * 지역별 벤더사 통계
+     */
+    public function getRegionStats()
+    {
+        return $this->select('REGION, COUNT(*) as count')
+                   ->where('IS_ACT', 'Y')
+                   ->groupBy('REGION')
+                   ->findAll();
+    }
+}

+ 2 - 5
components/common/header.vue

@@ -110,11 +110,8 @@
   };
 
   const fnLoguOut = () => {
-    localStorage.removeItem("tempAccess");
-    useAuthStore().setLogout();
-    router.push({
-      path: "/",
-    });
+    const { logout } = useLogout();
+    logout();
   };
 
   const myPage = (userId) => {

+ 2 - 3
components/common/leftMenu.vue

@@ -237,9 +237,8 @@ function fnMoveHome() {
 }
 // 로그아웃
 function fnLogout() {
-  localStorage.removeItem("authStore");
-  useAuthStore().setLogout();
-  useUtil.setPageMove("/");
+  const { logout } = useLogout()
+  logout()
 }
 </script>
 

+ 54 - 0
composables/useLogout.js

@@ -0,0 +1,54 @@
+import { useAuthStore } from '~/stores/auth'
+import { useVendorsStore } from '~/stores/vendors'
+import { useDetailStore } from '~/stores/detail'
+import { useRouter } from 'vue-router'
+
+export const useLogout = () => {
+  const authStore = useAuthStore()
+  const vendorsStore = useVendorsStore()
+  const detailStore = useDetailStore()
+  const router = useRouter()
+  const { $toast } = useNuxtApp()
+
+  const logout = async () => {
+    try {
+      // 현재 로그인 타입 저장 (로그아웃 전에 미리 저장)
+      const loginType = authStore.auth.snsTempData?.logintype || 'vendor' // 기본값은 vendor
+      
+      // API 호출로 서버 세션 종료 (선택적)
+      await useAxios().post('/api/auth/logout')
+      
+      // auth store 초기화
+      authStore.setLogout()
+      
+      // vendors store 초기화
+      vendorsStore.$reset()
+      
+      // detail store 초기화  
+      detailStore.$reset()
+      
+      // localStorage 정리
+      localStorage.removeItem('authStore')
+      localStorage.removeItem('tempAccess')
+      
+      // 성공 메시지 표시
+      $toast.success('로그아웃되었습니다.')
+      
+      // 로그인 타입에 따라 적절한 페이지로 리다이렉트
+      router.push(`/?type=${loginType}`)
+    } catch (error) {
+      console.error('로그아웃 중 오류 발생:', error)
+      // 오류가 발생해도 로컬 상태는 정리
+      const loginType = authStore.auth.snsTempData?.logintype || 'vendor'
+      authStore.setLogout()
+      vendorsStore.$reset()
+      detailStore.$reset()
+      localStorage.clear()
+      router.push(`/?type=${loginType}`)
+    }
+  }
+  
+  return {
+    logout
+  }
+} 

+ 47 - 0
database/migrations/create_vendor_influencer_mapping.sql

@@ -0,0 +1,47 @@
+-- 벤더사-인플루언서 승인 매핑 테이블
+CREATE TABLE `VENDOR_INFLUENCER_MAPPING` (
+  `SEQ` int(11) NOT NULL AUTO_INCREMENT,
+  `VENDOR_SEQ` int(11) NOT NULL COMMENT '벤더사 SEQ (VENDOR_LIST.SEQ 참조)',
+  `INFLUENCER_SEQ` int(11) NOT NULL COMMENT '인플루언서 SEQ (USER_LIST.SEQ 참조)',
+  `REQUEST_TYPE` varchar(20) NOT NULL DEFAULT 'INFLUENCER_REQUEST' COMMENT '요청 타입: INFLUENCER_REQUEST(인플루언서 요청), VENDOR_INVITE(벤더사 초대)',
+  `STATUS` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT '승인 상태: PENDING(대기), APPROVED(승인), REJECTED(거절), CANCELLED(취소)',
+  `REQUEST_MESSAGE` text DEFAULT NULL COMMENT '요청 메시지',
+  `RESPONSE_MESSAGE` text DEFAULT NULL COMMENT '응답 메시지',
+  `REQUESTED_BY` int(11) NOT NULL COMMENT '요청자 SEQ',
+  `APPROVED_BY` int(11) DEFAULT NULL COMMENT '승인자 SEQ',
+  `REQUEST_DATE` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '요청일시',
+  `RESPONSE_DATE` timestamp NULL DEFAULT NULL COMMENT '응답일시',
+  `EXPIRED_DATE` timestamp NULL DEFAULT NULL COMMENT '만료일시',
+  `PARTNERSHIP_START_DATE` timestamp NULL DEFAULT NULL COMMENT '파트너십 시작일',
+  `PARTNERSHIP_END_DATE` timestamp NULL DEFAULT NULL COMMENT '파트너십 종료일',
+  `COMMISSION_RATE` decimal(5,2) DEFAULT NULL COMMENT '수수료율 (%)',
+  `SPECIAL_CONDITIONS` text DEFAULT NULL COMMENT '특별 조건',
+  `IS_ACTIVE` varchar(1) NOT NULL DEFAULT 'Y' COMMENT '활성 상태: Y(활성), N(비활성)',
+  `CREATED_AT` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '생성일시',
+  `UPDATED_AT` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '수정일시',
+  `ADD_INFO1` varchar(500) DEFAULT NULL COMMENT '추가정보1',
+  `ADD_INFO2` varchar(500) DEFAULT NULL COMMENT '추가정보2',
+  `ADD_INFO3` varchar(500) DEFAULT NULL COMMENT '추가정보3',
+  PRIMARY KEY (`SEQ`),
+  UNIQUE KEY `unique_vendor_influencer` (`VENDOR_SEQ`, `INFLUENCER_SEQ`, `STATUS`),
+  KEY `idx_vendor_seq` (`VENDOR_SEQ`),
+  KEY `idx_influencer_seq` (`INFLUENCER_SEQ`),
+  KEY `idx_status` (`STATUS`),
+  KEY `idx_request_type` (`REQUEST_TYPE`),
+  KEY `idx_is_active` (`IS_ACTIVE`),
+  KEY `idx_request_date` (`REQUEST_DATE`),
+  CONSTRAINT `fk_vendor_mapping` FOREIGN KEY (`VENDOR_SEQ`) REFERENCES `VENDOR_LIST` (`SEQ`) ON DELETE CASCADE,
+  CONSTRAINT `fk_influencer_mapping` FOREIGN KEY (`INFLUENCER_SEQ`) REFERENCES `USER_LIST` (`SEQ`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci COMMENT='벤더사-인플루언서 승인 매핑 테이블';
+
+-- 인덱스 추가 설명
+-- unique_vendor_influencer: 동일한 벤더사-인플루언서 조합에서 동일한 상태의 중복 방지
+-- 다른 상태로는 여러 레코드 허용 (예: 이전 거절 후 재요청)
+
+-- 샘플 데이터 (테스트용)
+-- INSERT INTO `VENDOR_INFLUENCER_MAPPING` 
+-- (`VENDOR_SEQ`, `INFLUENCER_SEQ`, `REQUEST_TYPE`, `STATUS`, `REQUEST_MESSAGE`, `REQUESTED_BY`) 
+-- VALUES 
+-- (1, 1, 'INFLUENCER_REQUEST', 'PENDING', '귀하의 제품에 관심이 있어 파트너십을 요청합니다.', 1),
+-- (1, 2, 'VENDOR_INVITE', 'APPROVED', '저희 브랜드와 함께 해주세요.', 1),
+-- (2, 1, 'INFLUENCER_REQUEST', 'REJECTED', '협업 요청드립니다.', 1);

+ 11 - 0
database/url.md

@@ -0,0 +1,11 @@
+# url 리스트
+
+## 인플루언서 ##
+### /vender/serach : 벤더사 리스트 표시 및 검색통하여 승인요청하는 페이지 구성
+
+## 벤더사 ##
+### /view/vendor/dashboard/influencer-requests : 인플루언서가 요청한 승인관련 부분을 처리
+
+
+
+

+ 31 - 0
ddl/001_create_vendor_influencer_mapping_table.sql

@@ -0,0 +1,31 @@
+-- DDL 001: 벤더사-인플루언서 승인 매핑 테이블 생성
+-- 생성일: 2025-07-22
+-- 목적: 벤더사와 인플루언서 간의 승인 요청 및 파트너십 매핑 관리
+
+CREATE TABLE `VENDOR_INFLUENCER_MAPPING` (
+  `SEQ` int(11) NOT NULL AUTO_INCREMENT COMMENT '기본키',
+  `VENDOR_SEQ` int(11) NOT NULL COMMENT '벤더사 SEQ (VENDOR_LIST.SEQ 참조)',
+  `INFLUENCER_SEQ` int(11) NOT NULL COMMENT '인플루언서 SEQ (USER_LIST.SEQ 참조)',
+  `REQUEST_TYPE` varchar(20) NOT NULL DEFAULT 'INFLUENCER_REQUEST' COMMENT '요청 타입: INFLUENCER_REQUEST(인플루언서 요청), VENDOR_INVITE(벤더사 초대)',
+  `STATUS` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT '승인 상태: PENDING(대기), APPROVED(승인), REJECTED(거절), CANCELLED(취소)',
+  `REQUEST_MESSAGE` text DEFAULT NULL COMMENT '요청 메시지',
+  `RESPONSE_MESSAGE` text DEFAULT NULL COMMENT '응답 메시지',
+  `REQUESTED_BY` int(11) NOT NULL COMMENT '요청자 SEQ',
+  `APPROVED_BY` int(11) DEFAULT NULL COMMENT '승인자 SEQ',
+  `REQUEST_DATE` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '요청일시',
+  `RESPONSE_DATE` timestamp NULL DEFAULT NULL COMMENT '응답일시',
+  `EXPIRED_DATE` timestamp NULL DEFAULT NULL COMMENT '만료일시',
+  `PARTNERSHIP_START_DATE` timestamp NULL DEFAULT NULL COMMENT '파트너십 시작일',
+  `PARTNERSHIP_END_DATE` timestamp NULL DEFAULT NULL COMMENT '파트너십 종료일',
+  `COMMISSION_RATE` decimal(5,2) DEFAULT NULL COMMENT '수수료율 (%)',
+  `SPECIAL_CONDITIONS` text DEFAULT NULL COMMENT '특별 조건',
+  `IS_ACTIVE` varchar(1) NOT NULL DEFAULT 'Y' COMMENT '활성 상태: Y(활성), N(비활성)',
+  `CREATED_AT` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '생성일시',
+  `UPDATED_AT` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '수정일시',
+  `ADD_INFO1` varchar(500) DEFAULT NULL COMMENT '추가정보1',
+  `ADD_INFO2` varchar(500) DEFAULT NULL COMMENT '추가정보2',
+  `ADD_INFO3` varchar(500) DEFAULT NULL COMMENT '추가정보3',
+  PRIMARY KEY (`SEQ`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci COMMENT='벤더사-인플루언서 승인 매핑 테이블';
+
+-- 기본 인덱스 추가는 다음 DDL에서 처리

+ 39 - 0
ddl/002_add_vendor_influencer_mapping_indexes.sql

@@ -0,0 +1,39 @@
+-- DDL 002: 벤더사-인플루언서 매핑 테이블 인덱스 추가
+-- 생성일: 2025-07-22
+-- 목적: 성능 최적화를 위한 인덱스 및 제약 조건 추가
+
+-- 1. 기본 검색용 인덱스
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD INDEX `idx_vendor_seq` (`VENDOR_SEQ`);
+
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD INDEX `idx_influencer_seq` (`INFLUENCER_SEQ`);
+
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD INDEX `idx_status` (`STATUS`);
+
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD INDEX `idx_request_type` (`REQUEST_TYPE`);
+
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD INDEX `idx_is_active` (`IS_ACTIVE`);
+
+-- 2. 날짜 관련 인덱스
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD INDEX `idx_request_date` (`REQUEST_DATE`);
+
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD INDEX `idx_expired_date` (`EXPIRED_DATE`);
+
+-- 3. 복합 인덱스 (성능 최적화)
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD INDEX `idx_vendor_status_active` (`VENDOR_SEQ`, `STATUS`, `IS_ACTIVE`);
+
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD INDEX `idx_influencer_status_active` (`INFLUENCER_SEQ`, `STATUS`, `IS_ACTIVE`);
+
+-- 4. 중복 방지를 위한 유니크 인덱스
+-- 동일한 벤더사-인플루언서 조합에서 동일한 상태의 중복 방지
+-- 다른 상태로는 여러 레코드 허용 (예: 이전 거절 후 재요청)
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD UNIQUE INDEX `unique_vendor_influencer_status` (`VENDOR_SEQ`, `INFLUENCER_SEQ`, `STATUS`);

+ 27 - 0
ddl/003_add_vendor_influencer_mapping_foreign_keys.sql

@@ -0,0 +1,27 @@
+-- DDL 003: 벤더사-인플루언서 매핑 테이블 외래키 제약 조건 추가
+-- 생성일: 2025-07-22
+-- 목적: 데이터 무결성 보장을 위한 외래키 관계 설정
+
+-- 1. 벤더사 테이블과의 외래키 관계
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD CONSTRAINT `fk_vendor_mapping` 
+FOREIGN KEY (`VENDOR_SEQ`) REFERENCES `VENDOR_LIST` (`SEQ`) 
+ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- 2. 인플루언서(사용자) 테이블과의 외래키 관계
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD CONSTRAINT `fk_influencer_mapping` 
+FOREIGN KEY (`INFLUENCER_SEQ`) REFERENCES `USER_LIST` (`SEQ`) 
+ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- 3. 요청자와의 외래키 관계 (USER_LIST 참조)
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD CONSTRAINT `fk_requested_by_mapping` 
+FOREIGN KEY (`REQUESTED_BY`) REFERENCES `USER_LIST` (`SEQ`) 
+ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- 4. 승인자와의 외래키 관계 (USER_LIST 참조)
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+ADD CONSTRAINT `fk_approved_by_mapping` 
+FOREIGN KEY (`APPROVED_BY`) REFERENCES `USER_LIST` (`SEQ`) 
+ON DELETE RESTRICT ON UPDATE CASCADE;

+ 35 - 0
ddl/004_add_vendor_list_additional_columns.sql

@@ -0,0 +1,35 @@
+-- DDL 004: VENDOR_LIST 테이블에 추가 컬럼 추가
+-- 생성일: 2025-07-22
+-- 목적: 벤더사 검색 및 분류를 위한 추가 정보 컬럼
+
+-- 1. 카테고리 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `CATEGORY` varchar(50) DEFAULT NULL COMMENT '사업 카테고리: FASHION_BEAUTY, FOOD_HEALTH, LIFESTYLE, TECH_ELECTRONICS, SPORTS_LEISURE, CULTURE_ENTERTAINMENT';
+
+-- 2. 지역 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `REGION` varchar(50) DEFAULT NULL COMMENT '지역: SEOUL, GYEONGGI, INCHEON, BUSAN, DAEGU, DAEJEON, GWANGJU, ULSAN, OTHER';
+
+-- 3. 사업자 설명 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `DESCRIPTION` text DEFAULT NULL COMMENT '벤더사 설명';
+
+-- 4. 로고 이미지 URL 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `LOGO` varchar(500) DEFAULT NULL COMMENT '로고 이미지 URL';
+
+-- 5. 태그 컬럼 추가 (검색용)
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `TAGS` varchar(500) DEFAULT NULL COMMENT '검색 태그 (콤마 구분)';
+
+-- 6. 승인 상태 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `APPROVAL_STATUS` varchar(20) DEFAULT 'PENDING' COMMENT '승인 상태: PENDING, APPROVED, REJECTED';
+
+-- 7. 승인일 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `APPROVED_DATE` timestamp NULL DEFAULT NULL COMMENT '승인일시';
+
+-- 8. 최종 로그인 일시 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `LAST_LOGIN_DATE` timestamp NULL DEFAULT NULL COMMENT '최종 로그인 일시';

+ 91 - 0
ddl/004_add_vendor_list_additional_columns_safe.sql

@@ -0,0 +1,91 @@
+-- DDL 004: VENDOR_LIST 테이블에 추가 컬럼 추가 (안전 버전)
+-- 생성일: 2025-07-22
+-- 목적: 벤더사 검색 및 분류를 위한 추가 정보 컬럼 (중복 방지)
+
+-- 1. 카테고리 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND COLUMN_NAME = 'CATEGORY') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `CATEGORY` varchar(50) DEFAULT NULL COMMENT ''사업 카테고리: FASHION_BEAUTY, FOOD_HEALTH, LIFESTYLE, TECH_ELECTRONICS, SPORTS_LEISURE, CULTURE_ENTERTAINMENT''',
+    'SELECT "CATEGORY column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 2. 지역 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND COLUMN_NAME = 'REGION') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `REGION` varchar(50) DEFAULT NULL COMMENT ''지역: SEOUL, GYEONGGI, INCHEON, BUSAN, DAEGU, DAEJEON, GWANGJU, ULSAN, OTHER''',
+    'SELECT "REGION column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 3. 사업자 설명 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND COLUMN_NAME = 'DESCRIPTION') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `DESCRIPTION` text DEFAULT NULL COMMENT ''벤더사 설명''',
+    'SELECT "DESCRIPTION column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 4. 로고 이미지 URL 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND COLUMN_NAME = 'LOGO') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `LOGO` varchar(500) DEFAULT NULL COMMENT ''로고 이미지 URL''',
+    'SELECT "LOGO column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 5. 태그 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND COLUMN_NAME = 'TAGS') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `TAGS` varchar(500) DEFAULT NULL COMMENT ''검색 태그 (콤마 구분)''',
+    'SELECT "TAGS column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 6. 승인 상태 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND COLUMN_NAME = 'APPROVAL_STATUS') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVAL_STATUS` varchar(20) DEFAULT ''PENDING'' COMMENT ''승인 상태: PENDING, APPROVED, REJECTED''',
+    'SELECT "APPROVAL_STATUS column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 7. 승인일 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND COLUMN_NAME = 'APPROVED_DATE') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVED_DATE` timestamp NULL DEFAULT NULL COMMENT ''승인일시''',
+    'SELECT "APPROVED_DATE column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 8. 최종 로그인 일시 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND COLUMN_NAME = 'LAST_LOGIN_DATE') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `LAST_LOGIN_DATE` timestamp NULL DEFAULT NULL COMMENT ''최종 로그인 일시''',
+    'SELECT "LAST_LOGIN_DATE column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;

+ 38 - 0
ddl/004_add_vendor_list_additional_columns_simple.sql

@@ -0,0 +1,38 @@
+-- DDL 004: VENDOR_LIST 테이블에 추가 컬럼 추가 (단순 버전)
+-- 생성일: 2025-07-22
+-- 목적: 벤더사 검색 및 분류를 위한 추가 정보 컬럼
+
+-- 주의: 이미 컬럼이 존재하는 경우 에러가 발생할 수 있습니다.
+-- 에러가 발생하면 해당 컬럼은 이미 존재하는 것이므로 무시하고 계속 진행하세요.
+
+-- 1. 카테고리 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `CATEGORY` varchar(50) DEFAULT NULL COMMENT '사업 카테고리: FASHION_BEAUTY, FOOD_HEALTH, LIFESTYLE, TECH_ELECTRONICS, SPORTS_LEISURE, CULTURE_ENTERTAINMENT';
+
+-- 2. 지역 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `REGION` varchar(50) DEFAULT NULL COMMENT '지역: SEOUL, GYEONGGI, INCHEON, BUSAN, DAEGU, DAEJEON, GWANGJU, ULSAN, OTHER';
+
+-- 3. 사업자 설명 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `DESCRIPTION` text DEFAULT NULL COMMENT '벤더사 설명';
+
+-- 4. 로고 이미지 URL 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `LOGO` varchar(500) DEFAULT NULL COMMENT '로고 이미지 URL';
+
+-- 5. 태그 컬럼 추가 (검색용)
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `TAGS` varchar(500) DEFAULT NULL COMMENT '검색 태그 (콤마 구분)';
+
+-- 6. 승인 상태 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `APPROVAL_STATUS` varchar(20) DEFAULT 'PENDING' COMMENT '승인 상태: PENDING, APPROVED, REJECTED';
+
+-- 7. 승인일 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `APPROVED_DATE` timestamp NULL DEFAULT NULL COMMENT '승인일시';
+
+-- 8. 최종 로그인 일시 컬럼 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD COLUMN `LAST_LOGIN_DATE` timestamp NULL DEFAULT NULL COMMENT '최종 로그인 일시';

+ 34 - 0
ddl/004_vendor_check_and_add_columns.sql

@@ -0,0 +1,34 @@
+-- DDL 004: VENDOR_LIST 개별 컬럼 추가 (선택적 실행)
+-- 필요한 컬럼만 개별적으로 실행하세요
+
+-- 현재 컬럼 확인용 쿼리 (먼저 실행해서 어떤 컬럼이 있는지 확인)
+-- DESCRIBE VENDOR_LIST;
+
+-- 또는 다음 쿼리로 특정 컬럼 존재 여부 확인
+-- SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'VENDOR_LIST' AND TABLE_SCHEMA = DATABASE();
+
+-- 아래 각 컬럼을 개별적으로 실행하세요 (이미 존재하는 컬럼은 건너뛰세요)
+
+-- 1. 카테고리 컬럼
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `CATEGORY` varchar(50) DEFAULT NULL COMMENT '사업 카테고리: FASHION_BEAUTY, FOOD_HEALTH, LIFESTYLE, TECH_ELECTRONICS, SPORTS_LEISURE, CULTURE_ENTERTAINMENT';
+
+-- 2. 지역 컬럼
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `REGION` varchar(50) DEFAULT NULL COMMENT '지역: SEOUL, GYEONGGI, INCHEON, BUSAN, DAEGU, DAEJEON, GWANGJU, ULSAN, OTHER';
+
+-- 3. 사업자 설명 컬럼
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `DESCRIPTION` text DEFAULT NULL COMMENT '벤더사 설명';
+
+-- 4. 로고 이미지 URL 컬럼
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `LOGO` varchar(500) DEFAULT NULL COMMENT '로고 이미지 URL';
+
+-- 5. 태그 컬럼
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `TAGS` varchar(500) DEFAULT NULL COMMENT '검색 태그 (콤마 구분)';
+
+-- 6. 승인 상태 컬럼
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVAL_STATUS` varchar(20) DEFAULT 'PENDING' COMMENT '승인 상태: PENDING, APPROVED, REJECTED';
+
+-- 7. 승인일 컬럼
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVED_DATE` timestamp NULL DEFAULT NULL COMMENT '승인일시';
+
+-- 8. 최종 로그인 일시 컬럼 (이미 존재할 가능성 높음)
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `LAST_LOGIN_DATE` timestamp NULL DEFAULT NULL COMMENT '최종 로그인 일시';

+ 64 - 0
ddl/005_add_user_list_additional_columns.sql

@@ -0,0 +1,64 @@
+-- DDL 005: USER_LIST 테이블에 추가 컬럼 추가
+-- 생성일: 2025-07-22
+-- 목적: 인플루언서 분류 및 관리를 위한 추가 정보 컬럼
+
+-- 1. 인플루언서 타입 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `INFLUENCER_TYPE` varchar(50) DEFAULT NULL COMMENT '인플루언서 타입: MICRO, MACRO, MEGA, CELEBRITY';
+
+-- 2. 주요 카테고리 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `PRIMARY_CATEGORY` varchar(50) DEFAULT NULL COMMENT '주요 활동 카테고리: FASHION_BEAUTY, FOOD_HEALTH, LIFESTYLE, TECH_ELECTRONICS, SPORTS_LEISURE, CULTURE_ENTERTAINMENT';
+
+-- 3. 팔로워 수 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `FOLLOWER_COUNT` int(11) DEFAULT 0 COMMENT '총 팔로워 수';
+
+-- 4. 평균 조회수 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `AVG_VIEWS` int(11) DEFAULT 0 COMMENT '평균 조회수';
+
+-- 5. 프로필 이미지 URL 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `PROFILE_IMAGE` varchar(500) DEFAULT NULL COMMENT '프로필 이미지 URL';
+
+-- 6. 소개 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `BIO` text DEFAULT NULL COMMENT '자기소개';
+
+-- 7. 인스타그램 링크 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `INSTAGRAM_URL` varchar(200) DEFAULT NULL COMMENT '인스타그램 링크';
+
+-- 8. 유튜브 링크 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `YOUTUBE_URL` varchar(200) DEFAULT NULL COMMENT '유튜브 링크';
+
+-- 9. 틱톡 링크 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `TIKTOK_URL` varchar(200) DEFAULT NULL COMMENT '틱톡 링크';
+
+-- 10. 블로그 링크 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `BLOG_URL` varchar(200) DEFAULT NULL COMMENT '블로그 링크';
+
+-- 11. 선호 지역 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `PREFERRED_REGION` varchar(100) DEFAULT NULL COMMENT '선호 활동 지역';
+
+-- 12. 최소 수수료율 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `MIN_COMMISSION_RATE` decimal(5,2) DEFAULT NULL COMMENT '최소 수수료율 (%)';
+
+-- 13. 인증 상태 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `VERIFICATION_STATUS` varchar(20) DEFAULT 'UNVERIFIED' COMMENT '인증 상태: UNVERIFIED, PENDING, VERIFIED';
+
+-- 14. 인증일 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `VERIFIED_DATE` timestamp NULL DEFAULT NULL COMMENT '인증일시';
+
+-- 15. 최종 로그인 일시 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `LAST_LOGIN_DATE` timestamp NULL DEFAULT NULL COMMENT '최종 로그인 일시';
+

+ 168 - 0
ddl/005_add_user_list_additional_columns_safe.sql

@@ -0,0 +1,168 @@
+-- DDL 005: USER_LIST 테이블에 추가 컬럼 추가 (안전 버전)
+-- 생성일: 2025-07-22
+-- 목적: 인플루언서 분류 및 관리를 위한 추가 정보 컬럼 (중복 방지)
+
+-- 1. 인플루언서 타입 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'INFLUENCER_TYPE') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `INFLUENCER_TYPE` varchar(50) DEFAULT NULL COMMENT ''인플루언서 타입: MICRO, MACRO, MEGA, CELEBRITY''',
+    'SELECT "INFLUENCER_TYPE column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 2. 주요 카테고리 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'PRIMARY_CATEGORY') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `PRIMARY_CATEGORY` varchar(50) DEFAULT NULL COMMENT ''주요 활동 카테고리: FASHION_BEAUTY, FOOD_HEALTH, LIFESTYLE, TECH_ELECTRONICS, SPORTS_LEISURE, CULTURE_ENTERTAINMENT''',
+    'SELECT "PRIMARY_CATEGORY column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 3. 팔로워 수 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'FOLLOWER_COUNT') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `FOLLOWER_COUNT` int(11) DEFAULT 0 COMMENT ''총 팔로워 수''',
+    'SELECT "FOLLOWER_COUNT column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 4. 평균 조회수 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'AVG_VIEWS') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `AVG_VIEWS` int(11) DEFAULT 0 COMMENT ''평균 조회수''',
+    'SELECT "AVG_VIEWS column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 5. 프로필 이미지 URL 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'PROFILE_IMAGE') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `PROFILE_IMAGE` varchar(500) DEFAULT NULL COMMENT ''프로필 이미지 URL''',
+    'SELECT "PROFILE_IMAGE column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 6. 소개 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'BIO') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `BIO` text DEFAULT NULL COMMENT ''자기소개''',
+    'SELECT "BIO column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 7. 인스타그램 링크 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'INSTAGRAM_URL') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `INSTAGRAM_URL` varchar(200) DEFAULT NULL COMMENT ''인스타그램 링크''',
+    'SELECT "INSTAGRAM_URL column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 8. 유튜브 링크 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'YOUTUBE_URL') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `YOUTUBE_URL` varchar(200) DEFAULT NULL COMMENT ''유튜브 링크''',
+    'SELECT "YOUTUBE_URL column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 9. 틱톡 링크 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'TIKTOK_URL') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `TIKTOK_URL` varchar(200) DEFAULT NULL COMMENT ''틱톡 링크''',
+    'SELECT "TIKTOK_URL column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 10. 블로그 링크 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'BLOG_URL') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `BLOG_URL` varchar(200) DEFAULT NULL COMMENT ''블로그 링크''',
+    'SELECT "BLOG_URL column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 11. 선호 지역 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'PREFERRED_REGION') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `PREFERRED_REGION` varchar(100) DEFAULT NULL COMMENT ''선호 활동 지역''',
+    'SELECT "PREFERRED_REGION column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 12. 최소 수수료율 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'MIN_COMMISSION_RATE') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `MIN_COMMISSION_RATE` decimal(5,2) DEFAULT NULL COMMENT ''최소 수수료율 (%)''',
+    'SELECT "MIN_COMMISSION_RATE column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 13. 인증 상태 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'VERIFICATION_STATUS') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `VERIFICATION_STATUS` varchar(20) DEFAULT ''UNVERIFIED'' COMMENT ''인증 상태: UNVERIFIED, PENDING, VERIFIED''',
+    'SELECT "VERIFICATION_STATUS column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 14. 인증일 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'VERIFIED_DATE') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `VERIFIED_DATE` timestamp NULL DEFAULT NULL COMMENT ''인증일시''',
+    'SELECT "VERIFIED_DATE column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 15. 최종 로그인 일시 컬럼 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'LAST_LOGIN_DATE') = 0,
+    'ALTER TABLE `USER_LIST` ADD COLUMN `LAST_LOGIN_DATE` timestamp NULL DEFAULT NULL COMMENT ''최종 로그인 일시''',
+    'SELECT "LAST_LOGIN_DATE column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;

+ 66 - 0
ddl/005_add_user_list_additional_columns_simple.sql

@@ -0,0 +1,66 @@
+-- DDL 005: USER_LIST 테이블에 추가 컬럼 추가 (단순 버전)
+-- 생성일: 2025-07-22
+-- 목적: 인플루언서 분류 및 관리를 위한 추가 정보 컬럼
+
+-- 주의: 이미 컬럼이 존재하는 경우 에러가 발생할 수 있습니다.
+-- 에러가 발생하면 해당 컬럼은 이미 존재하는 것이므로 무시하고 계속 진행하세요.
+
+-- 1. 인플루언서 타입 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `INFLUENCER_TYPE` varchar(50) DEFAULT NULL COMMENT '인플루언서 타입: MICRO, MACRO, MEGA, CELEBRITY';
+
+-- 2. 주요 카테고리 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `PRIMARY_CATEGORY` varchar(50) DEFAULT NULL COMMENT '주요 활동 카테고리: FASHION_BEAUTY, FOOD_HEALTH, LIFESTYLE, TECH_ELECTRONICS, SPORTS_LEISURE, CULTURE_ENTERTAINMENT';
+
+-- 3. 팔로워 수 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `FOLLOWER_COUNT` int(11) DEFAULT 0 COMMENT '총 팔로워 수';
+
+-- 4. 평균 조회수 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `AVG_VIEWS` int(11) DEFAULT 0 COMMENT '평균 조회수';
+
+-- 5. 프로필 이미지 URL 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `PROFILE_IMAGE` varchar(500) DEFAULT NULL COMMENT '프로필 이미지 URL';
+
+-- 6. 소개 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `BIO` text DEFAULT NULL COMMENT '자기소개';
+
+-- 7. 인스타그램 링크 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `INSTAGRAM_URL` varchar(200) DEFAULT NULL COMMENT '인스타그램 링크';
+
+-- 8. 유튜브 링크 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `YOUTUBE_URL` varchar(200) DEFAULT NULL COMMENT '유튜브 링크';
+
+-- 9. 틱톡 링크 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `TIKTOK_URL` varchar(200) DEFAULT NULL COMMENT '틱톡 링크';
+
+-- 10. 블로그 링크 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `BLOG_URL` varchar(200) DEFAULT NULL COMMENT '블로그 링크';
+
+-- 11. 선호 지역 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `PREFERRED_REGION` varchar(100) DEFAULT NULL COMMENT '선호 활동 지역';
+
+-- 12. 최소 수수료율 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `MIN_COMMISSION_RATE` decimal(5,2) DEFAULT NULL COMMENT '최소 수수료율 (%)';
+
+-- 13. 인증 상태 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `VERIFICATION_STATUS` varchar(20) DEFAULT 'UNVERIFIED' COMMENT '인증 상태: UNVERIFIED, PENDING, VERIFIED';
+
+-- 14. 인증일 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `VERIFIED_DATE` timestamp NULL DEFAULT NULL COMMENT '인증일시';
+
+-- 15. 최종 로그인 일시 컬럼 추가
+ALTER TABLE `USER_LIST` 
+ADD COLUMN `LAST_LOGIN_DATE` timestamp NULL DEFAULT NULL COMMENT '최종 로그인 일시';

+ 32 - 0
ddl/006_create_partnership_history_table.sql

@@ -0,0 +1,32 @@
+-- DDL 006: 파트너십 이력 테이블 생성
+-- 생성일: 2025-07-22
+-- 목적: 벤더사-인플루언서 파트너십 활동 이력 추적
+
+CREATE TABLE `PARTNERSHIP_HISTORY` (
+  `SEQ` int(11) NOT NULL AUTO_INCREMENT COMMENT '기본키',
+  `MAPPING_SEQ` int(11) NOT NULL COMMENT '매핑 테이블 SEQ (VENDOR_INFLUENCER_MAPPING.SEQ 참조)',
+  `ACTION_TYPE` varchar(50) NOT NULL COMMENT '액션 타입: REQUEST_SENT, REQUEST_APPROVED, REQUEST_REJECTED, REQUEST_CANCELLED, PARTNERSHIP_STARTED, PARTNERSHIP_ENDED, CONTRACT_UPDATED',
+  `ACTION_BY` int(11) NOT NULL COMMENT '액션 수행자 SEQ (USER_LIST.SEQ 참조)',
+  `ACTION_DATE` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '액션 수행일시',
+  `DESCRIPTION` text DEFAULT NULL COMMENT '액션 설명',
+  `OLD_VALUE` text DEFAULT NULL COMMENT '변경 전 값 (JSON 형태)',
+  `NEW_VALUE` text DEFAULT NULL COMMENT '변경 후 값 (JSON 형태)',
+  `IP_ADDRESS` varchar(45) DEFAULT NULL COMMENT '수행자 IP 주소',
+  `USER_AGENT` varchar(500) DEFAULT NULL COMMENT '사용자 에이전트',
+  `ADD_INFO1` varchar(500) DEFAULT NULL COMMENT '추가정보1',
+  `ADD_INFO2` varchar(500) DEFAULT NULL COMMENT '추가정보2',
+  `ADD_INFO3` varchar(500) DEFAULT NULL COMMENT '추가정보3',
+  `CREATED_AT` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '생성일시',
+  PRIMARY KEY (`SEQ`),
+  INDEX `idx_mapping_seq` (`MAPPING_SEQ`),
+  INDEX `idx_action_type` (`ACTION_TYPE`),
+  INDEX `idx_action_by` (`ACTION_BY`),
+  INDEX `idx_action_date` (`ACTION_DATE`),
+  INDEX `idx_mapping_action_date` (`MAPPING_SEQ`, `ACTION_DATE`),
+  CONSTRAINT `fk_partnership_history_mapping` 
+    FOREIGN KEY (`MAPPING_SEQ`) REFERENCES `VENDOR_INFLUENCER_MAPPING` (`SEQ`) 
+    ON DELETE CASCADE ON UPDATE CASCADE,
+  CONSTRAINT `fk_partnership_history_user` 
+    FOREIGN KEY (`ACTION_BY`) REFERENCES `USER_LIST` (`SEQ`) 
+    ON DELETE RESTRICT ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci COMMENT='파트너십 이력 테이블';

+ 41 - 0
ddl/007_create_notification_table.sql

@@ -0,0 +1,41 @@
+-- DDL 007: 알림 테이블 생성
+-- 생성일: 2025-07-22
+-- 목적: 벤더사-인플루언서 간 알림 관리
+
+CREATE TABLE `NOTIFICATION` (
+  `SEQ` int(11) NOT NULL AUTO_INCREMENT COMMENT '기본키',
+  `RECIPIENT_SEQ` int(11) NOT NULL COMMENT '수신자 SEQ (USER_LIST.SEQ 참조)',
+  `SENDER_SEQ` int(11) DEFAULT NULL COMMENT '발신자 SEQ (USER_LIST.SEQ 참조)',
+  `TYPE` varchar(50) NOT NULL COMMENT '알림 타입: PARTNERSHIP_REQUEST, PARTNERSHIP_APPROVED, PARTNERSHIP_REJECTED, PARTNERSHIP_CANCELLED, PARTNERSHIP_EXPIRED, SYSTEM_NOTICE',
+  `TITLE` varchar(200) NOT NULL COMMENT '알림 제목',
+  `MESSAGE` text NOT NULL COMMENT '알림 메시지',
+  `RELATED_TYPE` varchar(50) DEFAULT NULL COMMENT '관련 테이블 타입: VENDOR_INFLUENCER_MAPPING, PARTNERSHIP_HISTORY',
+  `RELATED_SEQ` int(11) DEFAULT NULL COMMENT '관련 테이블 SEQ',
+  `IS_READ` varchar(1) NOT NULL DEFAULT 'N' COMMENT '읽음 상태: Y(읽음), N(안읽음)',
+  `READ_DATE` timestamp NULL DEFAULT NULL COMMENT '읽은 일시',
+  `PRIORITY` varchar(20) DEFAULT 'NORMAL' COMMENT '우선순위: HIGH, NORMAL, LOW',
+  `PUSH_SENT` varchar(1) DEFAULT 'N' COMMENT '푸시 발송 여부: Y(발송), N(미발송)',
+  `EMAIL_SENT` varchar(1) DEFAULT 'N' COMMENT '이메일 발송 여부: Y(발송), N(미발송)',
+  `SMS_SENT` varchar(1) DEFAULT 'N' COMMENT 'SMS 발송 여부: Y(발송), N(미발송)',
+  `EXPIRES_AT` timestamp NULL DEFAULT NULL COMMENT '만료일시',
+  `CREATED_AT` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '생성일시',
+  `UPDATED_AT` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '수정일시',
+  `ADD_INFO1` varchar(500) DEFAULT NULL COMMENT '추가정보1',
+  `ADD_INFO2` varchar(500) DEFAULT NULL COMMENT '추가정보2',
+  `ADD_INFO3` varchar(500) DEFAULT NULL COMMENT '추가정보3',
+  PRIMARY KEY (`SEQ`),
+  INDEX `idx_recipient_seq` (`RECIPIENT_SEQ`),
+  INDEX `idx_sender_seq` (`SENDER_SEQ`),
+  INDEX `idx_type` (`TYPE`),
+  INDEX `idx_is_read` (`IS_READ`),
+  INDEX `idx_priority` (`PRIORITY`),
+  INDEX `idx_created_at` (`CREATED_AT`),
+  INDEX `idx_recipient_read_created` (`RECIPIENT_SEQ`, `IS_READ`, `CREATED_AT`),
+  INDEX `idx_related_type_seq` (`RELATED_TYPE`, `RELATED_SEQ`),
+  CONSTRAINT `fk_notification_recipient` 
+    FOREIGN KEY (`RECIPIENT_SEQ`) REFERENCES `USER_LIST` (`SEQ`) 
+    ON DELETE CASCADE ON UPDATE CASCADE,
+  CONSTRAINT `fk_notification_sender` 
+    FOREIGN KEY (`SENDER_SEQ`) REFERENCES `USER_LIST` (`SEQ`) 
+    ON DELETE SET NULL ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci COMMENT='알림 테이블';

+ 74 - 0
ddl/008_create_sample_data_inserts.sql

@@ -0,0 +1,74 @@
+-- DDL 008: 샘플 데이터 삽입
+-- 생성일: 2025-07-22
+-- 목적: 테스트 및 개발을 위한 샘플 데이터
+
+-- 1. VENDOR_LIST 샘플 데이터 업데이트 (추가 컬럼 포함)
+UPDATE `VENDOR_LIST` SET 
+  `CATEGORY` = 'FASHION_BEAUTY',
+  `REGION` = 'SEOUL',
+  `DESCRIPTION` = '프리미엄 패션 브랜드입니다.',
+  `TAGS` = '패션,뷰티,프리미엄,트렌드',
+  `APPROVAL_STATUS` = 'APPROVED',
+  `APPROVED_DATE` = NOW()
+WHERE `SEQ` = 1;
+
+-- 2. USER_LIST 샘플 데이터 업데이트 (추가 컬럼 포함)
+UPDATE `USER_LIST` SET 
+  `INFLUENCER_TYPE` = 'MACRO',
+  `PRIMARY_CATEGORY` = 'FASHION_BEAUTY',
+  `FOLLOWER_COUNT` = 150000,
+  `AVG_VIEWS` = 25000,
+  `BIO` = '패션과 뷰티를 사랑하는 인플루언서입니다.',
+  `INSTAGRAM_URL` = 'https://instagram.com/sample_influencer',
+  `PREFERRED_REGION` = 'SEOUL,GYEONGGI',
+  `MIN_COMMISSION_RATE` = 15.0,
+  `VERIFICATION_STATUS` = 'VERIFIED',
+  `VERIFIED_DATE` = NOW()
+WHERE `SEQ` = 1;
+
+-- 3. VENDOR_INFLUENCER_MAPPING 샘플 데이터
+INSERT INTO `VENDOR_INFLUENCER_MAPPING` 
+(`VENDOR_SEQ`, `INFLUENCER_SEQ`, `REQUEST_TYPE`, `STATUS`, `REQUEST_MESSAGE`, `REQUESTED_BY`, `COMMISSION_RATE`, `SPECIAL_CONDITIONS`, `EXPIRED_DATE`) 
+VALUES 
+(1, 1, 'INFLUENCER_REQUEST', 'PENDING', '귀하의 브랜드에 관심이 있어 파트너십을 요청합니다.', 1, 20.0, '월 2회 포스팅 조건', DATE_ADD(NOW(), INTERVAL 7 DAY)),
+(1, 2, 'VENDOR_INVITE', 'APPROVED', '저희 브랜드와 함께 해주세요.', 1, 25.0, '독점 계약 조건', NULL),
+(2, 1, 'INFLUENCER_REQUEST', 'REJECTED', '협업 요청드립니다.', 1, 15.0, NULL, NULL);
+
+-- 4. PARTNERSHIP_HISTORY 샘플 데이터
+INSERT INTO `PARTNERSHIP_HISTORY` 
+(`MAPPING_SEQ`, `ACTION_TYPE`, `ACTION_BY`, `DESCRIPTION`, `IP_ADDRESS`) 
+VALUES 
+(1, 'REQUEST_SENT', 1, '인플루언서가 파트너십을 요청했습니다.', '127.0.0.1'),
+(2, 'REQUEST_APPROVED', 1, '벤더사가 파트너십 요청을 승인했습니다.', '127.0.0.1'),
+(3, 'REQUEST_REJECTED', 1, '벤더사가 파트너십 요청을 거절했습니다.', '127.0.0.1');
+
+-- 5. NOTIFICATION 샘플 데이터
+INSERT INTO `NOTIFICATION` 
+(`RECIPIENT_SEQ`, `SENDER_SEQ`, `TYPE`, `TITLE`, `MESSAGE`, `RELATED_TYPE`, `RELATED_SEQ`, `PRIORITY`) 
+VALUES 
+(1, NULL, 'PARTNERSHIP_REQUEST', '새로운 파트너십 요청', '새로운 벤더사로부터 파트너십 요청이 도착했습니다.', 'VENDOR_INFLUENCER_MAPPING', 1, 'HIGH'),
+(1, NULL, 'PARTNERSHIP_APPROVED', '파트너십 승인 완료', '요청하신 파트너십이 승인되었습니다.', 'VENDOR_INFLUENCER_MAPPING', 2, 'HIGH'),
+(1, NULL, 'PARTNERSHIP_REJECTED', '파트너십 요청 거절', '요청하신 파트너십이 거절되었습니다.', 'VENDOR_INFLUENCER_MAPPING', 3, 'NORMAL');
+
+-- 6. 추가 벤더사 샘플 데이터
+INSERT INTO `VENDOR_LIST` 
+(`NAME`, `COMPANY_NAME`, `COMPANY_NUMBER`, `ID`, `PASSWORD`, `HP`, `EMAIL`, `CATEGORY`, `REGION`, `DESCRIPTION`, `TAGS`, `APPROVAL_STATUS`, `APPROVED_DATE`, `MASTER_YN`, `IS_ACT`, `STATUS`) 
+VALUES 
+('김건강', '헬시푸드컴퍼니', '123-45-67891', 'healthyfood', 'password123', '010-2222-3333', 'contact@healthyfood.com', 'FOOD_HEALTH', 'BUSAN', '건강한 식품을 제조하는 회사입니다.', '건강식품,유기농,다이어트,웰빙', 'APPROVED', NOW(), '0', '1', '1'),
+('박라이프', '라이프스타일브랜드', '123-45-67892', 'lifestyle', 'password123', '010-3333-4444', 'contact@lifestyle.com', 'LIFESTYLE', 'GYEONGGI', '일상을 풍요롭게 하는 라이프스타일 제품을 판매합니다.', '라이프스타일,홈데코,생활용품', 'APPROVED', NOW(), '0', '1', '1'),
+('최테크', '테크혁신', '123-45-67893', 'techinno', 'password123', '010-4444-5555', 'contact@techinno.com', 'TECH_ELECTRONICS', 'DAEJEON', '혁신적인 기술 제품을 개발하는 회사입니다.', '기술,전자제품,혁신,스마트기기', 'PENDING', NULL, '0', '1', '1');
+
+-- 7. 추가 인플루언서 샘플 데이터
+INSERT INTO `USER_LIST` 
+(`ID`, `PASSWORD`, `NAME`, `TYPE`, `PHONE`, `EMAIL`, `NICK_NAME`, `INFLUENCER_TYPE`, `PRIMARY_CATEGORY`, `FOLLOWER_COUNT`, `AVG_VIEWS`, `BIO`, `INSTAGRAM_URL`, `PREFERRED_REGION`, `MIN_COMMISSION_RATE`, `VERIFICATION_STATUS`, `VERIFIED_DATE`, `IS_ACT`, `STATUS`, `MEMBER_TYPE`) 
+VALUES 
+('healthguru', 'password123', '건강전문가', '1', '010-5555-6666', 'health@example.com', '건강구루', 'MACRO', 'FOOD_HEALTH', 200000, 35000, '건강한 삶을 추구하는 인플루언서입니다.', 'https://instagram.com/healthguru', 'SEOUL,BUSAN', 18.0, 'VERIFIED', NOW(), '1', '1', 'INFLUENCER'),
+('techreview', 'password123', '기술리뷰어', '1', '010-6666-7777', 'tech@example.com', '테크리뷰', 'MICRO', 'TECH_ELECTRONICS', 75000, 15000, '최신 기술과 제품을 리뷰하는 인플루언서입니다.', 'https://instagram.com/techreview', 'DAEJEON,SEOUL', 12.0, 'VERIFIED', NOW(), '1', '1', 'INFLUENCER'),
+('lifestyler', 'password123', '라이프스타일러', '1', '010-7777-8888', 'lifestyle@example.com', '라이프러', 'MEGA', 'LIFESTYLE', 500000, 80000, '일상의 소소한 행복을 공유하는 인플루언서입니다.', 'https://instagram.com/lifestyler', 'GYEONGGI,SEOUL', 25.0, 'VERIFIED', NOW(), '1', '1', 'INFLUENCER');
+
+-- 인덱스 생성 완료 후 통계 정보 업데이트
+ANALYZE TABLE `VENDOR_INFLUENCER_MAPPING`;
+ANALYZE TABLE `PARTNERSHIP_HISTORY`;
+ANALYZE TABLE `NOTIFICATION`;
+ANALYZE TABLE `VENDOR_LIST`;
+ANALYZE TABLE `USER_LIST`;

+ 49 - 0
ddl/009_add_indexes_after_columns_exist.sql

@@ -0,0 +1,49 @@
+-- DDL 009: 컬럼 존재 확인 후 인덱스 추가
+-- 모든 필요한 컬럼이 추가된 후에 실행하세요
+
+-- 주의: 이 파일을 실행하기 전에 fix_missing_columns.sql을 먼저 실행하세요
+
+-- 1. 기본 인덱스들 (컬럼이 존재하는 경우에만 추가)
+-- CATEGORY 컬럼용 인덱스
+ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_category` (`CATEGORY`);
+
+-- REGION 컬럼용 인덱스
+ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_region` (`REGION`);
+
+-- APPROVAL_STATUS 컬럼용 인덱스
+ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_approval_status` (`APPROVAL_STATUS`);
+
+-- COMPANY_NAME 인덱스 (기존 컬럼)
+ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_company_name` (`COMPANY_NAME`);
+
+-- 2. USER_LIST 인덱스들
+-- INFLUENCER_TYPE 컬럼용 인덱스
+ALTER TABLE `USER_LIST` ADD INDEX `idx_influencer_type` (`INFLUENCER_TYPE`);
+
+-- PRIMARY_CATEGORY 컬럼용 인덱스
+ALTER TABLE `USER_LIST` ADD INDEX `idx_primary_category` (`PRIMARY_CATEGORY`);
+
+-- VERIFICATION_STATUS 컬럼용 인덱스
+ALTER TABLE `USER_LIST` ADD INDEX `idx_verification_status` (`VERIFICATION_STATUS`);
+
+-- FOLLOWER_COUNT 컬럼용 인덱스
+ALTER TABLE `USER_LIST` ADD INDEX `idx_follower_count` (`FOLLOWER_COUNT`);
+
+-- EMAIL 인덱스 (기존 컬럼)
+ALTER TABLE `USER_LIST` ADD INDEX `idx_email` (`EMAIL`);
+
+-- NICK_NAME 인덱스 (기존 컬럼)
+ALTER TABLE `USER_LIST` ADD INDEX `idx_nick_name` (`NICK_NAME`);
+
+-- 3. 복합 인덱스들 (모든 관련 컬럼이 존재할 때만 추가)
+-- VENDOR_LIST 복합 인덱스
+ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_category_region_status` (`CATEGORY`, `REGION`, `IS_ACT`);
+
+ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_approval_status_date` (`APPROVAL_STATUS`, `APPROVED_DATE`);
+
+-- USER_LIST 복합 인덱스
+ALTER TABLE `USER_LIST` ADD INDEX `idx_type_category_verified` (`INFLUENCER_TYPE`, `PRIMARY_CATEGORY`, `VERIFICATION_STATUS`);
+
+ALTER TABLE `USER_LIST` ADD INDEX `idx_category_follower_verified` (`PRIMARY_CATEGORY`, `FOLLOWER_COUNT`, `VERIFICATION_STATUS`);
+
+ALTER TABLE `USER_LIST` ADD INDEX `idx_member_type_active_status` (`MEMBER_TYPE`, `IS_ACT`, `STATUS`);

+ 28 - 0
ddl/009_add_vendor_list_indexes.sql

@@ -0,0 +1,28 @@
+-- DDL 009: VENDOR_LIST 테이블 인덱스 추가
+-- 생성일: 2025-07-22
+-- 목적: VENDOR_LIST 추가 컬럼에 대한 검색 최적화 인덱스
+-- 전제조건: 004번 DDL이 먼저 실행되어 컬럼이 존재해야 함
+
+-- 1. 새로 추가된 컬럼들에 대한 인덱스
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_category` (`CATEGORY`);
+
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_region` (`REGION`);
+
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_approval_status` (`APPROVAL_STATUS`);
+
+-- 2. 기존 컬럼 인덱스 추가 (중복 에러 방지를 위해 IF NOT EXISTS 사용)
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_company_name` (`COMPANY_NAME`);
+
+-- 3. 복합 인덱스 추가 (검색 성능 향상)
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_category_region_status` (`CATEGORY`, `REGION`, `IS_ACT`);
+
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_approval_status_date` (`APPROVAL_STATUS`, `APPROVED_DATE`);
+
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_region_category_active` (`REGION`, `CATEGORY`, `IS_ACT`, `STATUS`);

+ 80 - 0
ddl/009_add_vendor_list_indexes_safe.sql

@@ -0,0 +1,80 @@
+-- DDL 009: VENDOR_LIST 테이블 인덱스 추가 (안전 버전)
+-- 생성일: 2025-07-22
+-- 목적: VENDOR_LIST 추가 컬럼에 대한 검색 최적화 인덱스 (중복 방지)
+
+-- 1. CATEGORY 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND INDEX_NAME = 'idx_category') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_category` (`CATEGORY`)',
+    'SELECT "idx_category already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 2. REGION 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND INDEX_NAME = 'idx_region') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_region` (`REGION`)',
+    'SELECT "idx_region already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 3. APPROVAL_STATUS 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND INDEX_NAME = 'idx_approval_status') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_approval_status` (`APPROVAL_STATUS`)',
+    'SELECT "idx_approval_status already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 4. COMPANY_NAME 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND INDEX_NAME = 'idx_company_name') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_company_name` (`COMPANY_NAME`)',
+    'SELECT "idx_company_name already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 5. 복합 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND INDEX_NAME = 'idx_category_region_status') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_category_region_status` (`CATEGORY`, `REGION`, `IS_ACT`)',
+    'SELECT "idx_category_region_status already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 6. APPROVAL_STATUS, APPROVED_DATE 복합 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND INDEX_NAME = 'idx_approval_status_date') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_approval_status_date` (`APPROVAL_STATUS`, `APPROVED_DATE`)',
+    'SELECT "idx_approval_status_date already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 7. 추가 복합 인덱스 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND INDEX_NAME = 'idx_region_category_active') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD INDEX `idx_region_category_active` (`REGION`, `CATEGORY`, `IS_ACT`, `STATUS`)',
+    'SELECT "idx_region_category_active already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;

+ 30 - 0
ddl/009_add_vendor_list_indexes_simple.sql

@@ -0,0 +1,30 @@
+-- DDL 009: VENDOR_LIST 테이블 인덱스 추가 (단순 버전)
+-- 생성일: 2025-07-22
+-- 목적: VENDOR_LIST 추가 컬럼에 대한 검색 최적화 인덱스
+
+-- 주의: 이미 인덱스가 존재하는 경우 에러가 발생할 수 있습니다.
+-- 에러가 발생하면 해당 인덱스는 이미 존재하는 것이므로 무시하고 계속 진행하세요.
+
+-- 1. 새로 추가된 컬럼들에 대한 인덱스
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_category` (`CATEGORY`);
+
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_region` (`REGION`);
+
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_approval_status` (`APPROVAL_STATUS`);
+
+-- 2. 기존 컬럼 인덱스 추가
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_company_name` (`COMPANY_NAME`);
+
+-- 3. 복합 인덱스 추가 (검색 성능 향상)
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_category_region_status` (`CATEGORY`, `REGION`, `IS_ACT`);
+
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_approval_status_date` (`APPROVAL_STATUS`, `APPROVED_DATE`);
+
+ALTER TABLE `VENDOR_LIST` 
+ADD INDEX `idx_region_category_active` (`REGION`, `CATEGORY`, `IS_ACT`, `STATUS`);

+ 34 - 0
ddl/010_add_user_list_indexes.sql

@@ -0,0 +1,34 @@
+-- DDL 010: USER_LIST 테이블 인덱스 추가
+-- 생성일: 2025-07-22
+-- 목적: USER_LIST 추가 컬럼에 대한 검색 최적화 인덱스
+-- 전제조건: 005번 DDL이 먼저 실행되어 컬럼이 존재해야 함
+
+-- 1. 새로 추가된 컬럼들에 대한 인덱스
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_influencer_type` (`INFLUENCER_TYPE`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_primary_category` (`PRIMARY_CATEGORY`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_verification_status` (`VERIFICATION_STATUS`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_follower_count` (`FOLLOWER_COUNT`);
+
+-- 2. 기존 컬럼 인덱스 추가 (기본적인 검색용)
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_email` (`EMAIL`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_nick_name` (`NICK_NAME`);
+
+-- 3. 복합 인덱스 추가 (검색 성능 향상)
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_type_category_verified` (`INFLUENCER_TYPE`, `PRIMARY_CATEGORY`, `VERIFICATION_STATUS`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_category_follower_verified` (`PRIMARY_CATEGORY`, `FOLLOWER_COUNT`, `VERIFICATION_STATUS`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_member_type_active_status` (`MEMBER_TYPE`, `IS_ACT`, `STATUS`);

+ 102 - 0
ddl/010_add_user_list_indexes_safe.sql

@@ -0,0 +1,102 @@
+-- DDL 010: USER_LIST 테이블 인덱스 추가 (안전 버전)
+-- 생성일: 2025-07-22
+-- 목적: USER_LIST 추가 컬럼에 대한 검색 최적화 인덱스 (중복 방지)
+
+-- 1. INFLUENCER_TYPE 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND INDEX_NAME = 'idx_influencer_type') = 0,
+    'ALTER TABLE `USER_LIST` ADD INDEX `idx_influencer_type` (`INFLUENCER_TYPE`)',
+    'SELECT "idx_influencer_type already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 2. PRIMARY_CATEGORY 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND INDEX_NAME = 'idx_primary_category') = 0,
+    'ALTER TABLE `USER_LIST` ADD INDEX `idx_primary_category` (`PRIMARY_CATEGORY`)',
+    'SELECT "idx_primary_category already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 3. VERIFICATION_STATUS 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND INDEX_NAME = 'idx_verification_status') = 0,
+    'ALTER TABLE `USER_LIST` ADD INDEX `idx_verification_status` (`VERIFICATION_STATUS`)',
+    'SELECT "idx_verification_status already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 4. FOLLOWER_COUNT 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND INDEX_NAME = 'idx_follower_count') = 0,
+    'ALTER TABLE `USER_LIST` ADD INDEX `idx_follower_count` (`FOLLOWER_COUNT`)',
+    'SELECT "idx_follower_count already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 5. EMAIL 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND INDEX_NAME = 'idx_email') = 0,
+    'ALTER TABLE `USER_LIST` ADD INDEX `idx_email` (`EMAIL`)',
+    'SELECT "idx_email already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 6. NICK_NAME 인덱스 추가 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND INDEX_NAME = 'idx_nick_name') = 0,
+    'ALTER TABLE `USER_LIST` ADD INDEX `idx_nick_name` (`NICK_NAME`)',
+    'SELECT "idx_nick_name already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 7. 복합 인덱스: 타입, 카테고리, 인증상태 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND INDEX_NAME = 'idx_type_category_verified') = 0,
+    'ALTER TABLE `USER_LIST` ADD INDEX `idx_type_category_verified` (`INFLUENCER_TYPE`, `PRIMARY_CATEGORY`, `VERIFICATION_STATUS`)',
+    'SELECT "idx_type_category_verified already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 8. 복합 인덱스: 카테고리, 팔로워수, 인증상태 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND INDEX_NAME = 'idx_category_follower_verified') = 0,
+    'ALTER TABLE `USER_LIST` ADD INDEX `idx_category_follower_verified` (`PRIMARY_CATEGORY`, `FOLLOWER_COUNT`, `VERIFICATION_STATUS`)',
+    'SELECT "idx_category_follower_verified already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 9. 복합 인덱스: 멤버타입, 활성상태, 상태 (존재하지 않는 경우에만)
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'USER_LIST' AND INDEX_NAME = 'idx_member_type_active_status') = 0,
+    'ALTER TABLE `USER_LIST` ADD INDEX `idx_member_type_active_status` (`MEMBER_TYPE`, `IS_ACT`, `STATUS`)',
+    'SELECT "idx_member_type_active_status already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;

+ 36 - 0
ddl/010_add_user_list_indexes_simple.sql

@@ -0,0 +1,36 @@
+-- DDL 010: USER_LIST 테이블 인덱스 추가 (단순 버전)
+-- 생성일: 2025-07-22
+-- 목적: USER_LIST 추가 컬럼에 대한 검색 최적화 인덱스
+
+-- 주의: 이미 인덱스가 존재하는 경우 에러가 발생할 수 있습니다.
+-- 에러가 발생하면 해당 인덱스는 이미 존재하는 것이므로 무시하고 계속 진행하세요.
+
+-- 1. 새로 추가된 컬럼들에 대한 인덱스
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_influencer_type` (`INFLUENCER_TYPE`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_primary_category` (`PRIMARY_CATEGORY`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_verification_status` (`VERIFICATION_STATUS`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_follower_count` (`FOLLOWER_COUNT`);
+
+-- 2. 기존 컬럼 인덱스 추가 (기본적인 검색용)
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_email` (`EMAIL`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_nick_name` (`NICK_NAME`);
+
+-- 3. 복합 인덱스 추가 (검색 성능 향상)
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_type_category_verified` (`INFLUENCER_TYPE`, `PRIMARY_CATEGORY`, `VERIFICATION_STATUS`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_category_follower_verified` (`PRIMARY_CATEGORY`, `FOLLOWER_COUNT`, `VERIFICATION_STATUS`);
+
+ALTER TABLE `USER_LIST` 
+ADD INDEX `idx_member_type_active_status` (`MEMBER_TYPE`, `IS_ACT`, `STATUS`);

+ 20 - 0
ddl/011_fix_vendor_influencer_mapping_columns.sql

@@ -0,0 +1,20 @@
+-- DDL 011: VENDOR_INFLUENCER_MAPPING 테이블 컬럼 수정
+-- 목적: 기존 프로젝트 패턴에 맞게 컬럼명 통일
+
+-- 1. IS_ACTIVE를 IS_ACT로 변경 (기존 프로젝트 패턴 따름)
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+CHANGE COLUMN `IS_ACTIVE` `IS_ACT` varchar(1) NOT NULL DEFAULT 'Y' COMMENT '활성 상태: Y(활성), N(비활성)';
+
+-- 2. 기존 프로젝트 패턴에 맞게 타임스탬프 컬럼명 변경
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+CHANGE COLUMN `CREATED_AT` `REG_DATE` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '등록일시';
+
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+CHANGE COLUMN `UPDATED_AT` `MOD_DATE` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '수정일시';
+
+-- 3. REQUEST_DATE를 REG_DATE와 구분하기 위해 그대로 유지하되, 필요시 기본값 설정
+ALTER TABLE `VENDOR_INFLUENCER_MAPPING` 
+MODIFY COLUMN `REQUEST_DATE` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '요청일시';
+
+-- 4. 테이블 구조 확인용 쿼리 (참고용)
+-- DESCRIBE VENDOR_INFLUENCER_MAPPING;

+ 134 - 0
ddl/012_add_vendor_list_essential_columns.sql

@@ -0,0 +1,134 @@
+-- DDL 012: VENDOR_LIST 필수 컬럼 추가
+-- 목적: 벤더사 검색 기능에 필요한 컬럼들 추가
+
+-- 현재 테이블 구조 확인 (참고용)
+-- DESCRIBE VENDOR_LIST;
+
+-- 1. CATEGORY 컬럼 추가 (이미 존재하면 오류 발생하므로 개별 실행)
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `CATEGORY` varchar(50) DEFAULT NULL COMMENT '사업 카테고리: FASHION_BEAUTY, FOOD_HEALTH, LIFESTYLE, TECH_ELECTRONICS, SPORTS_LEISURE, CULTURE_ENTERTAINMENT';
+
+-- 2. REGION 컬럼 추가
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `REGION` varchar(50) DEFAULT NULL COMMENT '지역: SEOUL, GYEONGGI, INCHEON, BUSAN, DAEGU, DAEJEON, GWANGJU, ULSAN, OTHER';
+
+-- 3. DESCRIPTION 컬럼 추가
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `DESCRIPTION` text DEFAULT NULL COMMENT '벤더사 설명';
+
+-- 4. LOGO 컬럼 추가
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `LOGO` varchar(500) DEFAULT NULL COMMENT '로고 이미지 URL';
+
+-- 5. TAGS 컬럼 추가
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `TAGS` varchar(500) DEFAULT NULL COMMENT '검색 태그 (콤마 구분)';
+
+-- 6. APPROVAL_STATUS 컬럼 추가
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVAL_STATUS` varchar(20) DEFAULT 'PENDING' COMMENT '승인 상태: PENDING, APPROVED, REJECTED';
+
+-- 7. APPROVED_DATE 컬럼 추가
+-- ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVED_DATE` timestamp NULL DEFAULT NULL COMMENT '승인일시';
+
+-- ========================================
+-- 실행 방법 (DBeaver에서 개별 실행)
+-- ========================================
+
+-- 1단계: 현재 컬럼 확인
+SELECT COLUMN_NAME 
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_NAME = 'VENDOR_LIST' 
+AND TABLE_SCHEMA = DATABASE()
+ORDER BY ORDINAL_POSITION;
+
+-- 2단계: 없는 컬럼만 개별 실행
+-- 위 주석 처리된 ALTER TABLE 문들을 필요에 따라 개별 실행
+
+-- ========================================
+-- 한번에 실행하는 버전 (컬럼이 없는 경우에만)
+-- ========================================
+
+-- CATEGORY 컬럼 존재 확인 및 추가
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_NAME = 'VENDOR_LIST' 
+AND COLUMN_NAME = 'CATEGORY' 
+AND TABLE_SCHEMA = DATABASE();
+
+SET @sql = IF(@col_exists = 0, 
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `CATEGORY` varchar(50) DEFAULT NULL COMMENT ''사업 카테고리''', 
+    'SELECT ''CATEGORY column already exists'' as message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- REGION 컬럼 존재 확인 및 추가
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_NAME = 'VENDOR_LIST' 
+AND COLUMN_NAME = 'REGION' 
+AND TABLE_SCHEMA = DATABASE();
+
+SET @sql = IF(@col_exists = 0, 
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `REGION` varchar(50) DEFAULT NULL COMMENT ''지역''', 
+    'SELECT ''REGION column already exists'' as message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- DESCRIPTION 컬럼 존재 확인 및 추가
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_NAME = 'VENDOR_LIST' 
+AND COLUMN_NAME = 'DESCRIPTION' 
+AND TABLE_SCHEMA = DATABASE();
+
+SET @sql = IF(@col_exists = 0, 
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `DESCRIPTION` text DEFAULT NULL COMMENT ''벤더사 설명''', 
+    'SELECT ''DESCRIPTION column already exists'' as message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- TAGS 컬럼 존재 확인 및 추가
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_NAME = 'VENDOR_LIST' 
+AND COLUMN_NAME = 'TAGS' 
+AND TABLE_SCHEMA = DATABASE();
+
+SET @sql = IF(@col_exists = 0, 
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `TAGS` varchar(500) DEFAULT NULL COMMENT ''검색 태그''', 
+    'SELECT ''TAGS column already exists'' as message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- LOGO 컬럼 존재 확인 및 추가
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_NAME = 'VENDOR_LIST' 
+AND COLUMN_NAME = 'LOGO' 
+AND TABLE_SCHEMA = DATABASE();
+
+SET @sql = IF(@col_exists = 0, 
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `LOGO` varchar(500) DEFAULT NULL COMMENT ''로고 이미지 URL''', 
+    'SELECT ''LOGO column already exists'' as message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- APPROVAL_STATUS 컬럼 존재 확인 및 추가
+SET @col_exists = 0;
+SELECT COUNT(*) INTO @col_exists
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_NAME = 'VENDOR_LIST' 
+AND COLUMN_NAME = 'APPROVAL_STATUS' 
+AND TABLE_SCHEMA = DATABASE();
+
+SET @sql = IF(@col_exists = 0, 
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVAL_STATUS` varchar(20) DEFAULT ''PENDING'' COMMENT ''승인 상태''', 
+    'SELECT ''APPROVAL_STATUS column already exists'' as message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;

+ 25 - 0
ddl/013_simple_vendor_list_columns.sql

@@ -0,0 +1,25 @@
+-- DDL 013: VENDOR_LIST 컬럼 추가 (단순 버전)
+-- DBeaver에서 하나씩 실행하세요 (이미 존재하면 오류 발생)
+
+-- 1. DESCRIPTION 컬럼 추가 ⭐ 가장 중요
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `DESCRIPTION` text DEFAULT NULL COMMENT '벤더사 설명';
+
+-- 2. CATEGORY 컬럼 추가
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `CATEGORY` varchar(50) DEFAULT NULL COMMENT '사업 카테고리';
+
+-- 3. REGION 컬럼 추가
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `REGION` varchar(50) DEFAULT NULL COMMENT '지역';
+
+-- 4. TAGS 컬럼 추가
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `TAGS` varchar(500) DEFAULT NULL COMMENT '검색 태그';
+
+-- 5. LOGO 컬럼 추가
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `LOGO` varchar(500) DEFAULT NULL COMMENT '로고 이미지 URL';
+
+-- 6. APPROVAL_STATUS 컬럼 추가
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVAL_STATUS` varchar(20) DEFAULT 'APPROVED' COMMENT '승인 상태';
+
+-- ==========================================
+-- 실행 후 확인
+-- ==========================================
+DESCRIBE VENDOR_LIST;

+ 82 - 0
ddl/014_update_vendor_test_data.sql

@@ -0,0 +1,82 @@
+-- DDL 014: VENDOR_LIST 테스트 데이터 업데이트
+-- 목적: 검색 기능 테스트를 위한 샘플 데이터 추가
+
+-- 1. 현재 테이블 구조 확인
+DESCRIBE VENDOR_LIST;
+
+-- 2. 현재 데이터 확인
+SELECT SEQ, COMPANY_NAME, CATEGORY, REGION, DESCRIPTION, TAGS FROM VENDOR_LIST;
+
+-- 3. 기존 데이터에 검색 가능한 정보 추가
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'TECH_ELECTRONICS',
+    REGION = 'SEOUL', 
+    DESCRIPTION = '벤더 담당자 회사입니다. 다양한 IT 서비스를 제공합니다.',
+    TAGS = '벤더,IT,서비스,기술'
+WHERE SEQ = 1;
+
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'LIFESTYLE',
+    REGION = 'GYEONGGI',
+    DESCRIPTION = '테스트 회사입니다. 라이프스타일 관련 제품을 다룹니다.',
+    TAGS = '테스트,라이프스타일,제품'
+WHERE SEQ = 2;
+
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'FASHION_BEAUTY',
+    REGION = 'SEOUL',
+    DESCRIPTION = '인터스코프는 패션 뷰티 분야의 전문 회사입니다. 다양한 브랜드와 협업합니다.',
+    TAGS = '인터스코프,패션,뷰티,브랜드'
+WHERE SEQ = 3;
+
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'FOOD_HEALTH',
+    REGION = 'BUSAN',
+    DESCRIPTION = '김민수 회사는 건강한 식품을 전문으로 하는 벤더사입니다.',
+    TAGS = '김민수,식품,건강,음식'
+WHERE SEQ = 5;
+
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'CULTURE_ENTERTAINMENT',
+    REGION = 'SEOUL',
+    DESCRIPTION = '인터스코프 엔터테인먼트 부문으로 문화 콘텐츠를 제작합니다.',
+    TAGS = '엔터테인먼트,문화,콘텐츠'
+WHERE SEQ = 6;
+
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'SPORTS_LEISURE',
+    REGION = 'DAEGU',
+    DESCRIPTION = '테스트 회사로 스포츠 레저 관련 제품을 판매합니다.',
+    TAGS = '스포츠,레저,운동'
+WHERE SEQ = 7;
+
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'TECH_ELECTRONICS',
+    REGION = 'SEOUL',
+    DESCRIPTION = '관리자 회사로 IT 전자제품 전문 벤더입니다.',
+    TAGS = '관리자,IT,전자제품,기술'
+WHERE SEQ = 8;
+
+-- 4. 업데이트 결과 확인
+SELECT SEQ, COMPANY_NAME, CATEGORY, REGION, DESCRIPTION, TAGS 
+FROM VENDOR_LIST 
+ORDER BY SEQ;
+
+-- 5. 검색 테스트 쿼리들
+-- 키워드 검색 테스트
+SELECT SEQ, COMPANY_NAME, CATEGORY, REGION 
+FROM VENDOR_LIST 
+WHERE IS_ACT = 'Y' 
+AND (COMPANY_NAME LIKE '%인터스코프%' OR DESCRIPTION LIKE '%인터스코프%' OR TAGS LIKE '%인터스코프%');
+
+-- 카테고리 검색 테스트  
+SELECT SEQ, COMPANY_NAME, CATEGORY, REGION 
+FROM VENDOR_LIST 
+WHERE IS_ACT = 'Y' 
+AND CATEGORY = 'FASHION_BEAUTY';
+
+-- 지역 검색 테스트
+SELECT SEQ, COMPANY_NAME, CATEGORY, REGION 
+FROM VENDOR_LIST 
+WHERE IS_ACT = 'Y' 
+AND REGION = 'SEOUL';

+ 30 - 0
ddl/015_quick_vendor_data_update.sql

@@ -0,0 +1,30 @@
+-- DDL 015: VENDOR_LIST 빠른 데이터 업데이트
+-- DBeaver에서 바로 실행 가능
+
+-- 인터스코프 데이터 업데이트 (SEQ = 3)
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'FASHION_BEAUTY',
+    REGION = 'SEOUL',
+    DESCRIPTION = '인터스코프는 패션 뷰티 분야의 전문 회사입니다.',
+    TAGS = '인터스코프,패션,뷰티,브랜드,cosmetic'
+WHERE SEQ = 3;
+
+-- 다른 데이터들도 업데이트
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'TECH_ELECTRONICS',
+    REGION = 'SEOUL',
+    DESCRIPTION = '벤더 담당자 IT 서비스 회사',
+    TAGS = '벤더,IT,기술,서비스'
+WHERE SEQ = 1;
+
+UPDATE VENDOR_LIST SET 
+    CATEGORY = 'LIFESTYLE', 
+    REGION = 'GYEONGGI',
+    DESCRIPTION = '라이프스타일 테스트 회사',
+    TAGS = '테스트,라이프스타일'
+WHERE SEQ = 2;
+
+-- 업데이트 확인
+SELECT SEQ, COMPANY_NAME, CATEGORY, REGION, DESCRIPTION 
+FROM VENDOR_LIST 
+WHERE COMPANY_NAME LIKE '%인터스코프%';

+ 51 - 0
ddl/016_add_user_list_additional_columns.sql

@@ -0,0 +1,51 @@
+-- 1단계: REGION 컬럼 추가
+ALTER TABLE USER_LIST
+ADD COLUMN REGION VARCHAR(50) COMMENT '지역' AFTER EMAIL;
+
+-- 2단계: ENGAGEMENT_RATE 컬럼 추가
+ALTER TABLE USER_LIST
+ADD COLUMN ENGAGEMENT_RATE DECIMAL(5,2) COMMENT '참여율' AFTER FOLLOWER_COUNT;
+
+-- 3단계: DESCRIPTION 컬럼 추가
+ALTER TABLE USER_LIST
+ADD COLUMN DESCRIPTION TEXT COMMENT '소개글' AFTER ENGAGEMENT_RATE;
+
+-- 4단계: SNS_CHANNELS 컬럼 추가
+ALTER TABLE USER_LIST
+ADD COLUMN SNS_CHANNELS TEXT COMMENT 'SNS 채널 정보 (JSON)' AFTER DESCRIPTION;
+
+-- 5단계: 인덱스 추가
+ALTER TABLE USER_LIST
+ADD INDEX idx_region (REGION),
+ADD INDEX idx_follower_count (FOLLOWER_COUNT),
+ADD INDEX idx_engagement_rate (ENGAGEMENT_RATE);
+
+-- 6단계: 테스트 데이터 업데이트 (필요한 경우 실행)
+UPDATE USER_LIST
+SET 
+    REGION = CASE 
+        WHEN MOD(SEQ, 8) = 0 THEN 'SEOUL'
+        WHEN MOD(SEQ, 8) = 1 THEN 'BUSAN'
+        WHEN MOD(SEQ, 8) = 2 THEN 'INCHEON'
+        WHEN MOD(SEQ, 8) = 3 THEN 'DAEGU'
+        WHEN MOD(SEQ, 8) = 4 THEN 'DAEJEON'
+        WHEN MOD(SEQ, 8) = 5 THEN 'GWANGJU'
+        WHEN MOD(SEQ, 8) = 6 THEN 'ULSAN'
+        ELSE 'GYEONGGI'
+    END,
+    ENGAGEMENT_RATE = ROUND(RAND() * 10, 2),
+    DESCRIPTION = CASE 
+        WHEN PRIMARY_CATEGORY IS NOT NULL THEN
+            CASE 
+                WHEN PRIMARY_CATEGORY = 'FASHION_BEAUTY' THEN '패션과 뷰티 콘텐츠를 제작하는 인플루언서입니다.'
+                WHEN PRIMARY_CATEGORY = 'FOOD_HEALTH' THEN '맛집 탐방과 건강한 식단을 소개하는 푸드 크리에이터입니다.'
+                WHEN PRIMARY_CATEGORY = 'LIFESTYLE' THEN '일상의 특별한 순간을 공유하는 라이프스타일 크리에이터입니다.'
+                ELSE '다양한 콘텐츠를 제작하는 크리에이터입니다.'
+            END
+        ELSE '프로필 소개가 없습니다.'
+    END,
+    SNS_CHANNELS = CASE 
+        WHEN MOD(SEQ, 3) = 0 THEN CONCAT('[{"platform":"instagram","handle":"@user_', SEQ, '"},{"platform":"youtube","handle":"@creator_', SEQ, '"}]')
+        WHEN MOD(SEQ, 3) = 1 THEN CONCAT('[{"platform":"tiktok","handle":"@tiktok_', SEQ, '"},{"platform":"instagram","handle":"@insta_', SEQ, '"}]')
+        ELSE CONCAT('[{"platform":"youtube","handle":"@youtube_', SEQ, '"},{"platform":"blog","handle":"blog.creator', SEQ, '.com"}]')
+    END; 

+ 87 - 0
ddl/017_add_test_influencer_data.sql

@@ -0,0 +1,87 @@
+-- 테스트용 인플루언서 데이터 추가 (USER_LIST)
+INSERT INTO USER_LIST (
+    SEQ,
+    NICK_NAME,
+    NAME,
+    EMAIL,
+    REGION,
+    PRIMARY_CATEGORY,
+    DESCRIPTION,
+    SNS_CHANNELS,
+    FOLLOWER_COUNT,
+    ENGAGEMENT_RATE,
+    PROFILE_IMAGE,
+    IS_ACT
+) VALUES 
+(8,
+'패션요정', 
+'김패션',
+'fashion@example.com',
+'SEOUL',
+'FASHION_BEAUTY',
+'10년차 패션 크리에이터입니다. 트렌디한 스타일링과 뷰티 팁을 공유합니다.',
+'[{"platform":"instagram","handle":"@fashion_fairy"},{"platform":"youtube","handle":"@fashionfairy"}]',
+150000,
+5.8,
+NULL,
+'Y'
+),
+(9,
+'푸드마스터',
+'이맛남',
+'food@example.com',
+'BUSAN',
+'FOOD_HEALTH',
+'맛집 탐방과 레시피를 공유하는 푸드 크리에이터입니다.',
+'[{"platform":"instagram","handle":"@food_master"},{"platform":"blog","handle":"blog.foodmaster.com"}]',
+80000,
+4.2,
+NULL,
+'Y'
+),
+(10,
+'테크리뷰',
+'박테크',
+'tech@example.com',
+'SEOUL',
+'TECH_ELECTRONICS',
+'최신 전자기기와 가전제품을 리뷰하는 테크 인플루언서입니다.',
+'[{"platform":"youtube","handle":"@techreview"},{"platform":"instagram","handle":"@tech_review"}]',
+200000,
+6.5,
+NULL,
+'Y'
+);
+
+-- 테스트용 협업 이력 데이터 추가 (VENDOR_INFLUENCER_MAPPING)
+INSERT INTO VENDOR_INFLUENCER_MAPPING (
+    VENDOR_SEQ,
+    INFLUENCER_SEQ,
+    STATUS,
+    REQUEST_TYPE,
+    REQUEST_MESSAGE,
+    RESPONSE_MESSAGE,
+    COMMISSION_RATE,
+    REQUESTED_BY,
+    APPROVED_BY,
+    REQUEST_DATE,
+    RESPONSE_DATE,
+    IS_ACT
+) VALUES 
+-- 패션요정(8)이 벤더사(1)에 요청한 케이스
+(1, 8, 'APPROVED', 'INFLUENCER_REQUEST', '패션 제품 홍보를 하고 싶습니다.', '협업하게 되어 기쁩니다.', 10.0, 8, 1, NOW() - INTERVAL 30 DAY, NOW() - INTERVAL 29 DAY, 'Y'),
+
+-- 벤더사(2)가 패션요정(8)에게 요청한 케이스
+(2, 8, 'APPROVED', 'VENDOR_REQUEST', '뷰티 제품 홍보를 제안드립니다.', '함께 일하게 되어 기쁩니다.', 12.0, 2, 8, NOW() - INTERVAL 20 DAY, NOW() - INTERVAL 19 DAY, 'Y'),
+
+-- 푸드마스터(9)가 벤더사(3)에 요청한 케이스
+(3, 9, 'PENDING', 'INFLUENCER_REQUEST', '식품 리뷰를 하고 싶습니다.', NULL, 15.0, 9, NULL, NOW() - INTERVAL 5 DAY, NULL, 'Y'),
+
+-- 벤더사(4)가 푸드마스터(9)에게 요청한 케이스
+(4, 9, 'REJECTED', 'VENDOR_REQUEST', '건강식품 홍보를 제안드립니다.', '현재 다른 협업을 진행중입니다.', NULL, 4, 9, NOW() - INTERVAL 15 DAY, NOW() - INTERVAL 14 DAY, 'Y'),
+
+-- 테크리뷰(10)가 벤더사(5)에 요청한 케이스
+(5, 10, 'APPROVED', 'INFLUENCER_REQUEST', '가전제품 리뷰를 하고 싶습니다.', '제품 리뷰를 진행해주세요.', 15.0, 10, 5, NOW() - INTERVAL 40 DAY, NOW() - INTERVAL 39 DAY, 'Y'),
+
+-- 벤더사(6)이 테크리뷰(10)에게 요청한 케이스
+(6, 10, 'APPROVED', 'VENDOR_REQUEST', '신제품 홍보를 제안드립니다.', '협업을 진행하겠습니다.', 12.0, 6, 10, NOW() - INTERVAL 10 DAY, NOW() - INTERVAL 9 DAY, 'Y'); 

+ 114 - 0
ddl/README.md

@@ -0,0 +1,114 @@
+# DDL 실행 순서 가이드
+
+벤더사-인플루언서 시스템의 데이터베이스 스키마를 구축하기 위한 DDL 파일들의 실행 순서입니다.
+
+## 실행 순서
+
+### 1. 001_create_vendor_influencer_mapping_table.sql
+- **목적**: 벤더사-인플루언서 승인 매핑 테이블 생성
+- **의존성**: VENDOR_LIST, USER_LIST 테이블이 이미 존재해야 함
+- **설명**: 벤더사와 인플루언서 간의 승인 요청 및 파트너십 관리를 위한 핵심 테이블
+
+### 2. 002_add_vendor_influencer_mapping_indexes.sql
+- **목적**: 성능 최적화를 위한 인덱스 추가
+- **의존성**: 001번 DDL 실행 완료
+- **포함 인덱스**:
+  - 기본 검색용 인덱스 (VENDOR_SEQ, INFLUENCER_SEQ, STATUS 등)
+  - 복합 인덱스 (성능 최적화)
+  - 유니크 인덱스 (중복 방지)
+
+### 3. 003_add_vendor_influencer_mapping_foreign_keys.sql
+- **목적**: 데이터 무결성을 위한 외래키 제약 조건 추가
+- **의존성**: 001, 002번 DDL 실행 완료
+- **외래키 관계**:
+  - VENDOR_LIST와의 관계
+  - USER_LIST와의 관계 (인플루언서, 요청자, 승인자)
+
+### 4. 004_add_vendor_list_additional_columns.sql
+- **목적**: VENDOR_LIST 테이블에 검색 및 분류를 위한 컬럼 추가
+- **의존성**: 기존 VENDOR_LIST 테이블 존재
+- **추가 컬럼**:
+  - CATEGORY (사업 카테고리)
+  - REGION (지역)
+  - DESCRIPTION (설명)
+  - LOGO (로고 URL)
+  - TAGS (검색 태그)
+  - APPROVAL_STATUS (승인 상태)
+  - 기타 관리용 컬럼들
+
+### 5. 005_add_user_list_additional_columns.sql
+- **목적**: USER_LIST 테이블에 인플루언서 정보를 위한 컬럼 추가
+- **의존성**: 기존 USER_LIST 테이블 존재
+- **추가 컬럼**:
+  - INFLUENCER_TYPE (인플루언서 타입)
+  - PRIMARY_CATEGORY (주요 활동 카테고리)
+  - FOLLOWER_COUNT (팔로워 수)
+  - 소셜미디어 링크들
+  - 프로필 정보
+  - 인증 관련 컬럼들
+
+### 6. 006_create_partnership_history_table.sql
+- **목적**: 파트너십 활동 이력 추적을 위한 테이블 생성
+- **의존성**: 001~005번 DDL 실행 완료
+- **기능**: 모든 파트너십 관련 액션을 로깅하여 감사 추적 가능
+
+### 7. 007_create_notification_table.sql
+- **목적**: 알림 시스템을 위한 테이블 생성
+- **의존성**: 001~006번 DDL 실행 완료
+- **기능**: 파트너십 관련 알림 및 시스템 공지사항 관리
+
+### 8. 008_create_sample_data_inserts.sql
+- **목적**: 테스트 및 개발을 위한 샘플 데이터 삽입
+- **의존성**: 001~007번 DDL 실행 완료
+- **포함 데이터**:
+  - 벤더사 샘플 데이터
+  - 인플루언서 샘플 데이터
+  - 매핑 관계 샘플 데이터
+  - 이력 및 알림 샘플 데이터
+
+### 9. 009_add_vendor_list_indexes.sql
+- **목적**: VENDOR_LIST 테이블 검색 최적화 인덱스 추가
+- **의존성**: 004번 DDL 실행 완료 (컬럼 추가 후)
+- **포함 인덱스**: 카테고리, 지역, 승인상태 등
+
+### 10. 010_add_user_list_indexes.sql
+- **목적**: USER_LIST 테이블 검색 최적화 인덱스 추가
+- **의존성**: 005번 DDL 실행 완료 (컬럼 추가 후)
+- **포함 인덱스**: 인플루언서타입, 카테고리, 인증상태 등
+
+## 실행 방법
+
+```bash
+# MySQL 명령어로 순차 실행
+mysql -u [username] -p [database_name] < ddl/001_create_vendor_influencer_mapping_table.sql
+mysql -u [username] -p [database_name] < ddl/002_add_vendor_influencer_mapping_indexes.sql
+mysql -u [username] -p [database_name] < ddl/003_add_vendor_influencer_mapping_foreign_keys.sql
+mysql -u [username] -p [database_name] < ddl/004_add_vendor_list_additional_columns.sql
+mysql -u [username] -p [database_name] < ddl/005_add_user_list_additional_columns.sql
+mysql -u [username] -p [database_name] < ddl/006_create_partnership_history_table.sql
+mysql -u [username] -p [database_name] < ddl/007_create_notification_table.sql
+mysql -u [username] -p [database_name] < ddl/008_create_sample_data_inserts.sql
+mysql -u [username] -p [database_name] < ddl/009_add_vendor_list_indexes.sql
+mysql -u [username] -p [database_name] < ddl/010_add_user_list_indexes.sql
+```
+
+## 주의사항
+
+1. **순서 중요**: 반드시 번호 순서대로 실행해야 합니다.
+2. **의존성 확인**: 각 DDL 파일의 전제조건을 확인하세요.
+   - 009번은 004번 실행 후
+   - 010번은 005번 실행 후
+3. **백업**: 프로덕션 환경에서는 실행 전 반드시 데이터베이스 백업을 수행하세요.
+4. **권한**: DDL 실행을 위한 적절한 데이터베이스 권한이 필요합니다.
+5. **테스트**: 개발 환경에서 먼저 테스트 후 프로덕션에 적용하세요.
+6. **샘플 데이터**: 008번 파일의 샘플 데이터는 개발/테스트 환경에서만 사용하세요.
+7. **인덱스 중복**: 기존에 동일한 이름의 인덱스가 있으면 에러가 발생할 수 있습니다.
+
+## 롤백 방법
+
+각 DDL 파일에 대응하는 롤백 스크립트가 필요한 경우, 역순으로 DROP TABLE, DROP INDEX, DROP CONSTRAINT 등을 실행해야 합니다.
+
+## 버전 관리
+
+- 각 DDL 파일의 상단에 생성일과 목적이 명시되어 있습니다.
+- 변경사항이 있을 경우 새로운 번호의 DDL 파일을 생성하여 관리합니다.

+ 110 - 0
ddl/README_SAFE.md

@@ -0,0 +1,110 @@
+# DDL 실행 순서 가이드 (안전 버전)
+
+벤더사-인플루언서 시스템의 데이터베이스 스키마를 구축하기 위한 DDL 파일들의 실행 순서입니다.
+
+## 중복 에러 방지 버전 (권장)
+
+기존 DDL에서 중복 컬럼/인덱스 에러가 발생하는 경우 아래 안전 버전을 사용하세요.
+
+### 실행 순서 (안전 버전)
+
+### 1단계: 기본 테이블 생성
+```bash
+mysql -u [username] -p [database_name] < ddl/001_create_vendor_influencer_mapping_table.sql
+mysql -u [username] -p [database_name] < ddl/002_add_vendor_influencer_mapping_indexes.sql
+mysql -u [username] -p [database_name] < ddl/003_add_vendor_influencer_mapping_foreign_keys.sql
+```
+
+### 2단계: 컬럼 추가 (안전 버전)
+```bash
+mysql -u [username] -p [database_name] < ddl/004_add_vendor_list_additional_columns_safe.sql
+mysql -u [username] -p [database_name] < ddl/005_add_user_list_additional_columns_safe.sql
+```
+
+### 3단계: 보조 테이블 생성
+```bash
+mysql -u [username] -p [database_name] < ddl/006_create_partnership_history_table.sql
+mysql -u [username] -p [database_name] < ddl/007_create_notification_table.sql
+mysql -u [username] -p [database_name] < ddl/008_create_sample_data_inserts.sql
+```
+
+### 4단계: 인덱스 추가 (안전 버전)
+```bash
+mysql -u [username] -p [database_name] < ddl/009_add_vendor_list_indexes_safe.sql
+mysql -u [username] -p [database_name] < ddl/010_add_user_list_indexes_safe.sql
+```
+
+## 안전 버전 특징
+
+### 1. 컬럼 중복 방지
+- 각 컬럼 추가 전 `INFORMATION_SCHEMA.COLUMNS`에서 존재 여부 확인
+- 이미 존재하는 컬럼은 건너뛰고 정보 메시지 출력
+- 에러 없이 안전하게 실행 가능
+
+### 2. 인덱스 중복 방지
+- 각 인덱스 추가 전 `INFORMATION_SCHEMA.STATISTICS`에서 존재 여부 확인
+- 이미 존재하는 인덱스는 건너뛰고 정보 메시지 출력
+- 여러 번 실행해도 안전
+
+### 3. 동적 SQL 사용
+```sql
+SET @sql = IF(
+    (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+     WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'VENDOR_LIST' AND COLUMN_NAME = 'CATEGORY') = 0,
+    'ALTER TABLE `VENDOR_LIST` ADD COLUMN `CATEGORY` varchar(50) DEFAULT NULL',
+    'SELECT "CATEGORY column already exists" as info'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+```
+
+## 파일 설명
+
+### 안전 버전 파일들
+- **004_add_vendor_list_additional_columns_safe.sql**: VENDOR_LIST 컬럼 추가 (중복 방지)
+- **005_add_user_list_additional_columns_safe.sql**: USER_LIST 컬럼 추가 (중복 방지)
+- **009_add_vendor_list_indexes_safe.sql**: VENDOR_LIST 인덱스 추가 (중복 방지)
+- **010_add_user_list_indexes_safe.sql**: USER_LIST 인덱스 추가 (중복 방지)
+
+### 기존 버전 파일들
+- **004_add_vendor_list_additional_columns.sql**: 원본 버전 (중복 시 에러)
+- **005_add_user_list_additional_columns.sql**: 원본 버전 (중복 시 에러)
+- **009_add_vendor_list_indexes.sql**: 원본 버전 (중복 시 에러)
+- **010_add_user_list_indexes.sql**: 원본 버전 (중복 시 에러)
+
+## 사용 시나리오
+
+### 신규 설치
+- 안전 버전을 사용하여 에러 없이 설치
+
+### 기존 시스템 업데이트
+- 안전 버전을 사용하여 기존 데이터 보호하면서 업데이트
+
+### 개발/테스트 환경
+- 여러 번 실행이 필요한 경우 안전 버전 사용
+
+## 주의사항
+
+1. **순서 중요**: 반드시 번호 순서대로 실행해야 합니다.
+2. **백업**: 프로덕션 환경에서는 실행 전 반드시 데이터베이스 백업을 수행하세요.
+3. **권한**: DDL 실행을 위한 적절한 데이터베이스 권한이 필요합니다.
+4. **테스트**: 개발 환경에서 먼저 테스트 후 프로덕션에 적용하세요.
+5. **샘플 데이터**: 008번 파일의 샘플 데이터는 개발/테스트 환경에서만 사용하세요.
+6. **안전 버전 권장**: 중복 에러를 방지하려면 `_safe` 버전 사용을 권장합니다.
+
+## 에러 해결
+
+### "Duplicate column name" 에러
+- 안전 버전(`_safe`) 파일 사용
+
+### "Duplicate key name" 에러  
+- 안전 버전(`_safe`) 파일 사용
+
+### 권한 에러
+- DDL 실행 권한 확인
+- `ALTER`, `CREATE`, `INDEX` 권한 필요
+
+## 롤백
+
+필요 시 역순으로 `DROP COLUMN`, `DROP INDEX`, `DROP TABLE` 실행

+ 5 - 0
ddl/check_current_table_structure.sql

@@ -0,0 +1,5 @@
+-- USER_LIST 테이블 구조 확인
+SHOW COLUMNS FROM USER_LIST;
+
+-- VENDOR_INFLUENCER_MAPPING 테이블 구조 확인
+SHOW COLUMNS FROM VENDOR_INFLUENCER_MAPPING;

+ 75 - 0
ddl/fix_missing_columns.sql

@@ -0,0 +1,75 @@
+-- 누락된 컬럼들을 개별적으로 추가하는 DDL
+-- DBeaver에서 각 라인을 개별 실행하거나 필요한 것만 실행하세요
+
+-- 먼저 현재 VENDOR_LIST 테이블 구조 확인
+-- DESCRIBE VENDOR_LIST;
+
+-- 1. VENDOR_LIST에 누락될 수 있는 컬럼들 (개별 실행)
+
+-- CATEGORY 컬럼 (없다면 추가)
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `CATEGORY` varchar(50) DEFAULT NULL COMMENT '사업 카테고리';
+
+-- REGION 컬럼 (없다면 추가)
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `REGION` varchar(50) DEFAULT NULL COMMENT '지역';
+
+-- DESCRIPTION 컬럼 (없다면 추가)
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `DESCRIPTION` text DEFAULT NULL COMMENT '벤더사 설명';
+
+-- LOGO 컬럼 (없다면 추가)
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `LOGO` varchar(500) DEFAULT NULL COMMENT '로고 이미지 URL';
+
+-- TAGS 컬럼 (없다면 추가)
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `TAGS` varchar(500) DEFAULT NULL COMMENT '검색 태그';
+
+-- APPROVAL_STATUS 컬럼 (없다면 추가)
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVAL_STATUS` varchar(20) DEFAULT 'PENDING' COMMENT '승인 상태';
+
+-- APPROVED_DATE 컬럼 (없다면 추가)
+ALTER TABLE `VENDOR_LIST` ADD COLUMN `APPROVED_DATE` timestamp NULL DEFAULT NULL COMMENT '승인일시';
+
+-- 2. USER_LIST에 누락될 수 있는 컬럼들 (개별 실행)
+
+-- 먼저 현재 USER_LIST 테이블 구조 확인
+-- DESCRIBE USER_LIST;
+
+-- INFLUENCER_TYPE 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `INFLUENCER_TYPE` varchar(50) DEFAULT NULL COMMENT '인플루언서 타입';
+
+-- PRIMARY_CATEGORY 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `PRIMARY_CATEGORY` varchar(50) DEFAULT NULL COMMENT '주요 활동 카테고리';
+
+-- FOLLOWER_COUNT 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `FOLLOWER_COUNT` int(11) DEFAULT 0 COMMENT '총 팔로워 수';
+
+-- AVG_VIEWS 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `AVG_VIEWS` int(11) DEFAULT 0 COMMENT '평균 조회수';
+
+-- PROFILE_IMAGE 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `PROFILE_IMAGE` varchar(500) DEFAULT NULL COMMENT '프로필 이미지 URL';
+
+-- BIO 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `BIO` text DEFAULT NULL COMMENT '자기소개';
+
+-- INSTAGRAM_URL 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `INSTAGRAM_URL` varchar(200) DEFAULT NULL COMMENT '인스타그램 링크';
+
+-- YOUTUBE_URL 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `YOUTUBE_URL` varchar(200) DEFAULT NULL COMMENT '유튜브 링크';
+
+-- TIKTOK_URL 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `TIKTOK_URL` varchar(200) DEFAULT NULL COMMENT '틱톡 링크';
+
+-- BLOG_URL 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `BLOG_URL` varchar(200) DEFAULT NULL COMMENT '블로그 링크';
+
+-- PREFERRED_REGION 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `PREFERRED_REGION` varchar(100) DEFAULT NULL COMMENT '선호 활동 지역';
+
+-- MIN_COMMISSION_RATE 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `MIN_COMMISSION_RATE` decimal(5,2) DEFAULT NULL COMMENT '최소 수수료율';
+
+-- VERIFICATION_STATUS 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `VERIFICATION_STATUS` varchar(20) DEFAULT 'UNVERIFIED' COMMENT '인증 상태';
+
+-- VERIFIED_DATE 컬럼 (없다면 추가)
+ALTER TABLE `USER_LIST` ADD COLUMN `VERIFIED_DATE` timestamp NULL DEFAULT NULL COMMENT '인증일시';

+ 2 - 1
nuxt.config.ts

@@ -92,7 +92,8 @@ export default defineNuxtConfig({
   
   runtimeConfig: {
     public: {
-      anthropicApiKey: process.env.ANTHROPIC_API_KEY
+      anthropicApiKey: process.env.ANTHROPIC_API_KEY,
+      apiUrl: process.env.VITE_APP_API_URL
     }
   }
 })

+ 6 - 2
pages/index.vue

@@ -60,7 +60,9 @@
                 v-model="loginForm.userId"
                 placeholder="아이디를 입력해주세요"
                 class="custom-input"
-                @keyup.enter="loginAction(loginForm.userId, loginForm.passwd)"
+                @keyup.enter="
+                  loginAction(loginForm.userId, loginForm.passwd, loginForm.loginType)
+                "
                 @input="setInputField('main_userId')"
               ></v-text-field>
               <i class="ico"></i>
@@ -75,7 +77,9 @@
                 placeholder="비밀번호를 입력해주세요"
                 class="custom-input"
                 id="password"
-                @keyup.enter="loginAction(loginForm.userId, loginForm.passwd)"
+                @keyup.enter="
+                  loginAction(loginForm.userId, loginForm.passwd, loginForm.loginType)
+                "
                 @input="setInputField('main_passwd')"
               ></v-text-field>
               <i

+ 624 - 0
pages/view/influencer/[id].vue

@@ -0,0 +1,624 @@
+<template>
+  <div>
+    <div class="inner--headers">
+      <h2>인플루언서 프로필</h2>
+      <div class="bread--crumbs--wrap">
+        <span>홈</span>
+        <span>인플루언서</span>
+        <span>프로필</span>
+      </div>
+    </div>
+
+    <!-- 로딩 상태 -->
+    <div v-if="loading" class="loading-wrap">
+      <v-progress-circular indeterminate color="primary"></v-progress-circular>
+      <p>프로필을 불러오고 있습니다...</p>
+    </div>
+
+    <!-- 에러 상태 -->
+    <div v-else-if="error" class="error-wrap">
+      <v-alert type="error" dismissible @click:close="error = null">
+        {{ error }}
+      </v-alert>
+    </div>
+
+    <!-- 프로필 정보 -->
+    <div v-else-if="profile" class="profile--wrap">
+      <!-- 프로필 헤더 -->
+      <div class="profile--header">
+        <div class="profile--avatar">
+          <v-img
+            v-if="profile.PROFILE_IMAGE"
+            :src="profile.PROFILE_IMAGE"
+            :alt="profile.NICK_NAME + ' 프로필'"
+            width="120"
+            height="120"
+            cover
+          ></v-img>
+          <div v-else class="no-avatar">
+            {{ profile.NICK_NAME?.charAt(0) || "U" }}
+          </div>
+        </div>
+        <div class="profile--info">
+          <div class="profile--name">
+            <h3>{{ profile.NICK_NAME }}</h3>
+            <v-chip
+              v-if="profile.PRIMARY_CATEGORY"
+              color="primary"
+              size="small"
+              class="ml-2"
+            >
+              {{ getCategoryText(profile.PRIMARY_CATEGORY) }}
+            </v-chip>
+          </div>
+          <div class="profile--meta">
+            <div class="meta--item">
+              <v-icon size="small">mdi-account-group</v-icon>
+              <span>{{ formatNumber(profile.FOLLOWER_COUNT || 0) }} 팔로워</span>
+            </div>
+            <div class="meta--item">
+              <v-icon size="small">mdi-chart-line</v-icon>
+              <span>참여율 {{ profile.ENGAGEMENT_RATE || 0 }}%</span>
+            </div>
+            <div v-if="profile.REGION" class="meta--item">
+              <v-icon size="small">mdi-map-marker</v-icon>
+              <span>{{ profile.REGION }}</span>
+            </div>
+          </div>
+          <p v-if="profile.DESCRIPTION" class="profile--description">
+            {{ profile.DESCRIPTION }}
+          </p>
+        </div>
+      </div>
+
+      <!-- SNS 채널 -->
+      <div v-if="snsChannels.length > 0" class="profile--channels">
+        <h4>SNS 채널</h4>
+        <div class="channels--grid">
+          <a
+            v-for="channel in snsChannels"
+            :key="channel.platform"
+            :href="getSnsUrl(channel)"
+            target="_blank"
+            rel="noopener noreferrer"
+            class="channel--card"
+          >
+            <v-icon size="24" :color="getSnsColor(channel.platform)">
+              {{ getSnsIcon(channel.platform) }}
+            </v-icon>
+            <div class="channel--info">
+              <h5>{{ getSnsTitle(channel.platform) }}</h5>
+              <p>{{ channel.handle }}</p>
+            </div>
+            <v-icon size="16">mdi-open-in-new</v-icon>
+          </a>
+        </div>
+      </div>
+
+      <!-- 콘텐츠 통계 -->
+      <div class="profile--stats">
+        <h4>콘텐츠 통계</h4>
+        <div class="stats--grid">
+          <div class="stat--card">
+            <div class="stat--icon followers">
+              <v-icon>mdi-account-group</v-icon>
+            </div>
+            <div class="stat--content">
+              <h5>팔로워</h5>
+              <p>{{ formatNumber(profile.FOLLOWER_COUNT || 0) }}</p>
+            </div>
+          </div>
+          <div class="stat--card">
+            <div class="stat--icon engagement">
+              <v-icon>mdi-chart-line</v-icon>
+            </div>
+            <div class="stat--content">
+              <h5>참여율</h5>
+              <p>{{ profile.ENGAGEMENT_RATE || 0 }}%</p>
+            </div>
+          </div>
+          <div class="stat--card">
+            <div class="stat--icon partnerships">
+              <v-icon>mdi-handshake</v-icon>
+            </div>
+            <div class="stat--content">
+              <h5>협업</h5>
+              <p>{{ formatNumber(partnershipCount || 0) }}건</p>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 협업 이력 -->
+      <div v-if="partnerships.length > 0" class="profile--partnerships">
+        <h4>협업 이력</h4>
+        <div class="partnerships--timeline">
+          <div
+            v-for="partnership in partnerships"
+            :key="partnership.SEQ"
+            class="timeline--item"
+          >
+            <div class="timeline--date">
+              {{ formatDate(partnership.REG_DATE) }}
+            </div>
+            <div class="timeline--content">
+              <h5>{{ partnership.vendorName }}</h5>
+              <p v-if="partnership.DESCRIPTION">
+                {{ partnership.DESCRIPTION }}
+              </p>
+              <div class="timeline--meta">
+                <v-chip size="x-small" :color="getStatusColor(partnership.STATUS)">
+                  {{ getStatusText(partnership.STATUS) }}
+                </v-chip>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 데이터 없음 -->
+    <div v-else class="no-data-wrap">
+      <div class="no-data">
+        <v-icon size="64" color="grey-lighten-1">mdi-account-question</v-icon>
+        <h3>프로필을 찾을 수 없습니다</h3>
+        <p>요청하신 인플루언서 프로필이 존재하지 않습니다</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, onMounted } from "vue";
+  import { useRoute } from "vue-router";
+
+  /************************************************************************
+|    레이아웃
+************************************************************************/
+  definePageMeta({
+    layout: "default",
+  });
+
+  /************************************************************************
+|    스토어 & 라우터
+************************************************************************/
+  const route = useRoute();
+  const { $toast } = useNuxtApp();
+
+  /************************************************************************
+|    반응형 데이터
+************************************************************************/
+  const loading = ref(false);
+  const error = ref(null);
+  const profile = ref(null);
+  const partnerships = ref([]);
+  const partnershipCount = ref(0);
+
+  /************************************************************************
+|    computed
+************************************************************************/
+  const snsChannels = computed(() => {
+    if (!profile.value?.SNS_CHANNELS) return [];
+    try {
+      return JSON.parse(profile.value.SNS_CHANNELS);
+    } catch (e) {
+      return [];
+    }
+  });
+
+  /************************************************************************
+|    메서드
+************************************************************************/
+  const loadProfile = async () => {
+    try {
+      loading.value = true;
+      error.value = null;
+
+      const influencerSeq = route.params.id;
+      const params = { influencerSeq };
+
+      useAxios()
+        .post("/api/influencer/profile", params)
+        .then((res) => {
+          if (res.data.success) {
+            profile.value = res.data.data.profile;
+            partnerships.value = res.data.data.partnerships || [];
+            partnershipCount.value = res.data.data.partnershipCount || 0;
+          } else {
+            error.value = res.data.message || "프로필을 불러오는데 실패했습니다.";
+          }
+        })
+        .catch((err) => {
+          error.value = err.message || "프로필을 불러오는데 실패했습니다.";
+        })
+        .finally(() => {
+          loading.value = false;
+        });
+    } catch (err) {
+      error.value = err.message || "프로필을 불러오는데 실패했습니다.";
+      loading.value = false;
+    }
+  };
+
+  const getCategoryText = (category) => {
+    const categoryMap = {
+      FASHION_BEAUTY: "패션·뷰티",
+      FOOD_HEALTH: "식품·건강",
+      LIFESTYLE: "라이프스타일",
+      TECH_ELECTRONICS: "테크·가전",
+      SPORTS_LEISURE: "스포츠·레저",
+      CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
+    };
+    return categoryMap[category] || category || "기타";
+  };
+
+  const formatNumber = (num) => {
+    if (!num) return "0";
+    if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
+    if (num >= 1000) return (num / 1000).toFixed(1) + "K";
+    return num.toString();
+  };
+
+  const formatDate = (dateString) => {
+    return new Date(dateString).toLocaleDateString("ko-KR");
+  };
+
+  const getSnsIcon = (platform) => {
+    const iconMap = {
+      instagram: "mdi-instagram",
+      youtube: "mdi-youtube",
+      tiktok: "mdi-music-note",
+      blog: "mdi-post",
+      facebook: "mdi-facebook",
+      twitter: "mdi-twitter",
+    };
+    return iconMap[platform.toLowerCase()] || "mdi-link";
+  };
+
+  const getSnsColor = (platform) => {
+    const colorMap = {
+      instagram: "#E4405F",
+      youtube: "#FF0000",
+      tiktok: "#000000",
+      blog: "#00B336",
+      facebook: "#1877F2",
+      twitter: "#1DA1F2",
+    };
+    return colorMap[platform.toLowerCase()] || "#666666";
+  };
+
+  const getSnsTitle = (platform) => {
+    const titleMap = {
+      instagram: "Instagram",
+      youtube: "YouTube",
+      tiktok: "TikTok",
+      blog: "Blog",
+      facebook: "Facebook",
+      twitter: "Twitter",
+    };
+    return titleMap[platform] || platform;
+  };
+
+  const getSnsUrl = (channel) => {
+    const handle = channel.handle.replace("@", "");
+    const urlMap = {
+      instagram: `https://instagram.com/${handle}`,
+      youtube: `https://youtube.com/@${handle}`,
+      tiktok: `https://tiktok.com/@${handle}`,
+      blog: channel.handle.startsWith("http") ? channel.handle : `https://${handle}`,
+      facebook: `https://facebook.com/${handle}`,
+      twitter: `https://twitter.com/${handle}`,
+    };
+    return urlMap[channel.platform.toLowerCase()] || channel.handle;
+  };
+
+  const getStatusText = (status) => {
+    const statusMap = {
+      PENDING: "진행중",
+      APPROVED: "완료",
+      REJECTED: "거절됨",
+      CANCELLED: "취소됨",
+    };
+    return statusMap[status] || status || "알 수 없음";
+  };
+
+  const getStatusColor = (status) => {
+    const colorMap = {
+      PENDING: "warning",
+      APPROVED: "success",
+      REJECTED: "error",
+      CANCELLED: "grey",
+    };
+    return colorMap[status] || "grey";
+  };
+
+  /************************************************************************
+|    라이프사이클
+************************************************************************/
+  onMounted(() => {
+    loadProfile();
+  });
+</script>
+
+<style scoped>
+  .profile--wrap {
+    background: white;
+    border-radius: 12px;
+    padding: 24px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+
+  .profile--header {
+    display: flex;
+    gap: 24px;
+    margin-bottom: 32px;
+  }
+
+  .profile--avatar {
+    width: 120px;
+    height: 120px;
+    border-radius: 60px;
+    overflow: hidden;
+    flex-shrink: 0;
+    background: #f5f5f5;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .no-avatar {
+    font-size: 48px;
+    font-weight: bold;
+    color: #666;
+  }
+
+  .profile--info {
+    flex: 1;
+  }
+
+  .profile--name {
+    display: flex;
+    align-items: center;
+    margin-bottom: 12px;
+  }
+
+  .profile--name h3 {
+    margin: 0;
+    font-size: 24px;
+    font-weight: 600;
+  }
+
+  .profile--meta {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 16px;
+    margin-bottom: 16px;
+  }
+
+  .meta--item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    color: #666;
+  }
+
+  .profile--description {
+    font-size: 14px;
+    line-height: 1.6;
+    color: #444;
+    margin: 0;
+  }
+
+  .profile--channels {
+    margin-top: 32px;
+  }
+
+  .profile--channels h4,
+  .profile--stats h4,
+  .profile--partnerships h4 {
+    margin: 0 0 16px 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #333;
+  }
+
+  .channels--grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+    gap: 16px;
+  }
+
+  .channel--card {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 16px;
+    background: #f8f9fa;
+    border-radius: 8px;
+    text-decoration: none;
+    color: inherit;
+    transition: transform 0.2s, box-shadow 0.2s;
+  }
+
+  .channel--card:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+
+  .channel--info {
+    flex: 1;
+  }
+
+  .channel--info h5 {
+    margin: 0 0 4px 0;
+    font-size: 14px;
+    font-weight: 600;
+  }
+
+  .channel--info p {
+    margin: 0;
+    font-size: 13px;
+    color: #666;
+  }
+
+  .profile--stats {
+    margin-top: 32px;
+  }
+
+  .stats--grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+    gap: 16px;
+  }
+
+  .stat--card {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    padding: 20px;
+    background: #f8f9fa;
+    border-radius: 8px;
+  }
+
+  .stat--icon {
+    width: 48px;
+    height: 48px;
+    border-radius: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+  }
+
+  .stat--icon.followers {
+    background: #2196f3;
+  }
+
+  .stat--icon.engagement {
+    background: #4caf50;
+  }
+
+  .stat--icon.partnerships {
+    background: #ff9800;
+  }
+
+  .stat--content h5 {
+    margin: 0 0 4px 0;
+    font-size: 14px;
+    color: #666;
+  }
+
+  .stat--content p {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: #333;
+  }
+
+  .profile--partnerships {
+    margin-top: 32px;
+  }
+
+  .partnerships--timeline {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+  }
+
+  .timeline--item {
+    display: flex;
+    gap: 16px;
+  }
+
+  .timeline--date {
+    flex-shrink: 0;
+    width: 100px;
+    font-size: 14px;
+    color: #666;
+  }
+
+  .timeline--content {
+    flex: 1;
+    background: #f8f9fa;
+    padding: 16px;
+    border-radius: 8px;
+    position: relative;
+  }
+
+  .timeline--content::before {
+    content: "";
+    position: absolute;
+    left: -8px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 16px;
+    height: 16px;
+    background: #f8f9fa;
+    transform: rotate(45deg);
+  }
+
+  .timeline--content h5 {
+    margin: 0 0 8px 0;
+    font-size: 16px;
+    font-weight: 600;
+  }
+
+  .timeline--content p {
+    margin: 0 0 8px 0;
+    font-size: 14px;
+    color: #666;
+  }
+
+  .timeline--meta {
+    display: flex;
+    gap: 8px;
+  }
+
+  .loading-wrap,
+  .error-wrap,
+  .no-data-wrap {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+  }
+
+  .no-data {
+    text-align: center;
+  }
+
+  .no-data h3 {
+    margin: 16px 0 8px;
+    color: #666;
+  }
+
+  .no-data p {
+    color: #999;
+  }
+
+  @media (max-width: 768px) {
+    .profile--header {
+      flex-direction: column;
+      align-items: center;
+      text-align: center;
+    }
+
+    .profile--meta {
+      justify-content: center;
+    }
+
+    .timeline--item {
+      flex-direction: column;
+      gap: 8px;
+    }
+
+    .timeline--date {
+      width: auto;
+    }
+
+    .timeline--content::before {
+      display: none;
+    }
+  }
+</style>

+ 350 - 297
pages/view/vendor/[id].vue

@@ -5,20 +5,20 @@
       <div class="bread--crumbs--wrap">
         <span>홈</span>
         <span @click="goBack" class="breadcrumb-link">벤더사 관리</span>
-        <span>{{ currentVendor?.name || '벤더사 상세' }}</span>
+        <span>{{ currentVendor?.name || "벤더사 상세" }}</span>
       </div>
     </div>
 
     <!-- 로딩 상태 -->
-    <div v-if="vendorsStore.getLoading.value" class="loading-wrap">
+    <div v-if="isLoading" class="loading-wrap">
       <v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
       <p>벤더사 정보를 불러오고 있습니다...</p>
     </div>
 
     <!-- 에러 상태 -->
-    <div v-else-if="vendorsStore.getError.value" class="error-wrap">
+    <div v-else-if="errorMessage" class="error-wrap">
       <v-alert type="error" dismissible @click:close="vendorsStore.clearError()">
-        {{ vendorsStore.getError.value }}
+        {{ errorMessage }}
       </v-alert>
       <v-btn @click="goBack" class="custom-btn btn-blue">목록으로 돌아가기</v-btn>
     </div>
@@ -54,7 +54,7 @@
                   :color="currentVendor.status === 'ACTIVE' ? 'success' : 'error'"
                   size="large"
                 >
-                  {{ currentVendor.status === 'ACTIVE' ? '활성' : '비활성' }}
+                  {{ currentVendor.status === "ACTIVE" ? "활성" : "비활성" }}
                 </v-chip>
               </div>
               <p v-if="currentVendor.description" class="vendor-description">
@@ -101,25 +101,31 @@
                   <v-col cols="12" md="6">
                     <div class="info-item">
                       <h3>사업자등록번호</h3>
-                      <p>{{ currentVendor.businessNumber || '-' }}</p>
+                      <p>{{ currentVendor.businessNumber || "-" }}</p>
                     </div>
                   </v-col>
                   <v-col cols="12" md="6">
                     <div class="info-item">
                       <h3>설립일</h3>
-                      <p>{{ formatDate(currentVendor.establishedDate) || '-' }}</p>
+                      <p>{{ formatDate(currentVendor.establishedDate) || "-" }}</p>
                     </div>
                   </v-col>
                   <v-col cols="12" md="6">
                     <div class="info-item">
                       <h3>직원 수</h3>
-                      <p>{{ currentVendor.employeeCount ? currentVendor.employeeCount + '명' : '-' }}</p>
+                      <p>
+                        {{
+                          currentVendor.employeeCount
+                            ? currentVendor.employeeCount + "명"
+                            : "-"
+                        }}
+                      </p>
                     </div>
                   </v-col>
                   <v-col cols="12" md="6">
                     <div class="info-item">
                       <h3>연매출</h3>
-                      <p>{{ formatCurrency(currentVendor.annualRevenue) || '-' }}</p>
+                      <p>{{ formatCurrency(currentVendor.annualRevenue) || "-" }}</p>
                     </div>
                   </v-col>
                   <v-col cols="12">
@@ -154,21 +160,28 @@
                       </v-card-title>
                       <v-card-text>
                         <div class="contact-item">
-                          <strong>이름:</strong> {{ currentVendor.contactName || '-' }}
+                          <strong>이름:</strong> {{ currentVendor.contactName || "-" }}
                         </div>
                         <div class="contact-item">
-                          <strong>직책:</strong> {{ currentVendor.contactPosition || '-' }}
+                          <strong>직책:</strong>
+                          {{ currentVendor.contactPosition || "-" }}
                         </div>
                         <div class="contact-item">
-                          <strong>전화:</strong> 
-                          <a v-if="currentVendor.contactPhone" :href="`tel:${currentVendor.contactPhone}`">
+                          <strong>전화:</strong>
+                          <a
+                            v-if="currentVendor.contactPhone"
+                            :href="`tel:${currentVendor.contactPhone}`"
+                          >
                             {{ currentVendor.contactPhone }}
                           </a>
                           <span v-else>-</span>
                         </div>
                         <div class="contact-item">
                           <strong>이메일:</strong>
-                          <a v-if="currentVendor.contactEmail" :href="`mailto:${currentVendor.contactEmail}`">
+                          <a
+                            v-if="currentVendor.contactEmail"
+                            :href="`mailto:${currentVendor.contactEmail}`"
+                          >
                             {{ currentVendor.contactEmail }}
                           </a>
                           <span v-else>-</span>
@@ -185,14 +198,14 @@
                       <v-card-text>
                         <div class="contact-item">
                           <strong>주소:</strong>
-                          <p>{{ currentVendor.address || '-' }}</p>
+                          <p>{{ currentVendor.address || "-" }}</p>
                         </div>
                         <div class="contact-item">
                           <strong>상세주소:</strong>
-                          <p>{{ currentVendor.detailAddress || '-' }}</p>
+                          <p>{{ currentVendor.detailAddress || "-" }}</p>
                         </div>
                         <div class="contact-item">
-                          <strong>우편번호:</strong> {{ currentVendor.zipCode || '-' }}
+                          <strong>우편번호:</strong> {{ currentVendor.zipCode || "-" }}
                         </div>
                       </v-card-text>
                     </v-card>
@@ -257,7 +270,7 @@
                   <v-col cols="12" md="6">
                     <div class="info-item">
                       <h3>협력 시작일</h3>
-                      <p>{{ formatDate(currentVendor.partnershipStartDate) || '-' }}</p>
+                      <p>{{ formatDate(currentVendor.partnershipStartDate) || "-" }}</p>
                     </div>
                   </v-col>
                   <v-col cols="12" md="6">
@@ -283,7 +296,7 @@
                   <v-col cols="12">
                     <div class="info-item">
                       <h3>특이사항</h3>
-                      <p>{{ currentVendor.notes || '특이사항이 없습니다.' }}</p>
+                      <p>{{ currentVendor.notes || "특이사항이 없습니다." }}</p>
                     </div>
                   </v-col>
                 </v-row>
@@ -307,303 +320,343 @@
 </template>
 
 <script setup>
-import { ref, onMounted, computed } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { useVendorsStore } from '@/stores/vendors'
+  import { ref, onMounted, computed } from "vue";
+  import { useRoute, useRouter } from "vue-router";
+  import { useVendorsStore } from "@/stores/vendors";
 
-/************************************************************************
+  /************************************************************************
 |    레이아웃
 ************************************************************************/
-definePageMeta({
-  layout: "default",
-})
+  definePageMeta({
+    layout: "default",
+  });
 
-/************************************************************************
+  /************************************************************************
 |    스토어, 라우터, 라우트
 ************************************************************************/
-const vendorsStore = useVendorsStore()
-const router = useRouter()
-const route = useRoute()
+  const vendorsStore = useVendorsStore();
+  const router = useRouter();
+  const route = useRoute();
 
-/************************************************************************
+  /************************************************************************
 |    반응형 데이터
 ************************************************************************/
-const pageId = ref("벤더사 상세")
-const activeTab = ref("info")
+  const pageId = ref("벤더사 상세");
+  const activeTab = ref("info");
 
-/************************************************************************
-|    computed
+  /************************************************************************
+|    computed - 안전한 접근 방식
 ************************************************************************/
-const currentVendor = computed(() => vendorsStore.getCurrentVendor.value)
-
-/************************************************************************
+  const currentVendor = computed(() => {
+    try {
+      return vendorsStore.getCurrentVendor || null;
+    } catch (error) {
+      console.error("getCurrentVendor 접근 오류:", error);
+      return null;
+    }
+  });
+
+  const isLoading = computed(() => {
+    try {
+      return vendorsStore.getLoading || false;
+    } catch (error) {
+      console.error("getLoading 접근 오류:", error);
+      return false;
+    }
+  });
+
+  const errorMessage = computed(() => {
+    try {
+      return vendorsStore.getError || null;
+    } catch (error) {
+      console.error("getError 접근 오류:", error);
+      return null;
+    }
+  });
+
+  /************************************************************************
 |    메서드
 ************************************************************************/
-const goBack = () => {
-  router.push('/view/vendor/vendors')
-}
-
-const getCategoryColor = (category) => {
-  const colors = {
-    'FASHION_BEAUTY': 'pink',
-    'FOOD_HEALTH': 'green',
-    'LIFESTYLE': 'blue',
-    'TECH_ELECTRONICS': 'purple',
-    'SPORTS_LEISURE': 'orange',
-    'CULTURE_ENTERTAINMENT': 'red'
-  }
-  return colors[category] || 'grey'
-}
-
-const getCategoryName = (category) => {
-  const names = {
-    'FASHION_BEAUTY': '패션·뷰티',
-    'FOOD_HEALTH': '식품·건강',
-    'LIFESTYLE': '라이프스타일',
-    'TECH_ELECTRONICS': '테크·가전',
-    'SPORTS_LEISURE': '스포츠·레저',
-    'CULTURE_ENTERTAINMENT': '문화·엔터테인먼트'
-  }
-  return names[category] || category
-}
-
-const getPartnershipColor = (level) => {
-  const colors = {
-    'PLATINUM': 'purple',
-    'GOLD': 'amber',
-    'SILVER': 'grey',
-    'BRONZE': 'brown',
-    'BASIC': 'blue-grey'
-  }
-  return colors[level] || 'grey'
-}
-
-const getPartnershipName = (level) => {
-  const names = {
-    'PLATINUM': '플래티넘',
-    'GOLD': '골드',
-    'SILVER': '실버',
-    'BRONZE': '브론즈',
-    'BASIC': '베이직'
-  }
-  return names[level] || level
-}
-
-const formatDate = (dateString) => {
-  if (!dateString) return null
-  return new Date(dateString).toLocaleDateString('ko-KR')
-}
-
-const formatCurrency = (amount) => {
-  if (!amount) return null
-  return new Intl.NumberFormat('ko-KR', {
-    style: 'currency',
-    currency: 'KRW'
-  }).format(amount)
-}
-
-/************************************************************************
+  const goBack = () => {
+    router.push("/view/vendor/vendors");
+  };
+
+  const getCategoryColor = (category) => {
+    const colors = {
+      FASHION_BEAUTY: "pink",
+      FOOD_HEALTH: "green",
+      LIFESTYLE: "blue",
+      TECH_ELECTRONICS: "purple",
+      SPORTS_LEISURE: "orange",
+      CULTURE_ENTERTAINMENT: "red",
+    };
+    return colors[category] || "grey";
+  };
+
+  const getCategoryName = (category) => {
+    const names = {
+      FASHION_BEAUTY: "패션·뷰티",
+      FOOD_HEALTH: "식품·건강",
+      LIFESTYLE: "라이프스타일",
+      TECH_ELECTRONICS: "테크·가전",
+      SPORTS_LEISURE: "스포츠·레저",
+      CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
+    };
+    return names[category] || category;
+  };
+
+  const getPartnershipColor = (level) => {
+    const colors = {
+      PLATINUM: "purple",
+      GOLD: "amber",
+      SILVER: "grey",
+      BRONZE: "brown",
+      BASIC: "blue-grey",
+    };
+    return colors[level] || "grey";
+  };
+
+  const getPartnershipName = (level) => {
+    const names = {
+      PLATINUM: "플래티넘",
+      GOLD: "골드",
+      SILVER: "실버",
+      BRONZE: "브론즈",
+      BASIC: "베이직",
+    };
+    return names[level] || level;
+  };
+
+  const formatDate = (dateString) => {
+    if (!dateString) return null;
+    return new Date(dateString).toLocaleDateString("ko-KR");
+  };
+
+  const formatCurrency = (amount) => {
+    if (!amount) return null;
+    return new Intl.NumberFormat("ko-KR", {
+      style: "currency",
+      currency: "KRW",
+    }).format(amount);
+  };
+
+  /************************************************************************
 |    라이프사이클
 ************************************************************************/
-onMounted(async () => {
-  const vendorId = route.params.id
-  if (vendorId) {
-    await vendorsStore.getVendorById(vendorId)
-  }
-})
+  onMounted(async () => {
+    console.log("컴포넌트 마운트됨");
+    console.log("vendorsStore:", vendorsStore);
+    console.log("vendorsStore.getCurrentVendor:", vendorsStore.getCurrentVendor);
+    console.log("vendorsStore.getLoading:", vendorsStore.getLoading);
+    console.log("vendorsStore.getError:", vendorsStore.getError);
+
+    const vendorId = route.params.id;
+    console.log("vendorId:", vendorId);
+
+    if (vendorId) {
+      try {
+        console.log("API 호출 시작");
+        await vendorsStore.getVendorById(vendorId);
+        console.log("API 호출 완료");
+        console.log("currentVendor 값:", vendorsStore.getCurrentVendor);
+      } catch (error) {
+        console.error("벤더사 정보 로드 실패:", error);
+      }
+    }
+  });
 </script>
 
 <style scoped>
-.vendor-detail-wrap {
-  display: flex;
-  flex-direction: column;
-  gap: 20px;
-}
-
-.vendor-header-card {
-  margin-bottom: 20px;
-}
-
-.vendor-header {
-  display: flex;
-  align-items: flex-start;
-  gap: 20px;
-}
-
-.vendor-logo-section {
-  flex-shrink: 0;
-}
-
-.vendor-logo-large {
-  border: 1px solid #e0e0e0;
-}
-
-.no-logo-large {
-  background: #f5f5f5;
-  color: #666;
-  font-weight: bold;
-  font-size: 32px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-}
-
-.vendor-info-section {
-  flex: 1;
-}
-
-.vendor-name {
-  font-size: 28px;
-  font-weight: bold;
-  margin-bottom: 12px;
-}
-
-.vendor-meta {
-  margin-bottom: 16px;
-}
-
-.vendor-description {
-  color: #666;
-  line-height: 1.6;
-  margin: 0;
-}
-
-.vendor-actions {
-  flex-shrink: 0;
-}
-
-.breadcrumb-link {
-  cursor: pointer;
-  color: #1976d2;
-}
-
-.breadcrumb-link:hover {
-  text-decoration: underline;
-}
-
-.detail-tabs-card {
-  margin-top: 20px;
-}
-
-.info-section,
-.contact-section,
-.products-section,
-.partnership-section {
-  padding: 20px 0;
-}
-
-.info-item {
-  margin-bottom: 24px;
-}
-
-.info-item h3 {
-  font-size: 16px;
-  font-weight: 600;
-  margin-bottom: 8px;
-  color: #333;
-}
-
-.info-item p {
-  font-size: 14px;
-  color: #666;
-  margin: 0;
-}
-
-.contact-card {
-  height: 100%;
-}
-
-.contact-item {
-  margin-bottom: 12px;
-}
-
-.contact-item strong {
-  display: inline-block;
-  width: 80px;
-  color: #333;
-}
-
-.contact-item a {
-  color: #1976d2;
-  text-decoration: none;
-}
-
-.contact-item a:hover {
-  text-decoration: underline;
-}
-
-.section-header {
-  margin-bottom: 20px;
-  padding-bottom: 10px;
-  border-bottom: 1px solid #e0e0e0;
-}
-
-.section-header h3 {
-  font-size: 18px;
-  font-weight: 600;
-  margin: 0;
-}
-
-.product-card {
-  height: 100%;
-}
-
-.product-price {
-  font-weight: bold;
-  color: #1976d2;
-  margin-top: 8px;
-}
-
-.rating {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.rating-text {
-  font-size: 14px;
-  color: #666;
-}
-
-.business-areas {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 8px;
-}
-
-.loading-wrap,
-.error-wrap,
-.no-data-wrap {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  padding: 60px 20px;
-}
-
-.no-data {
-  text-align: center;
-}
-
-.no-data h3 {
-  margin: 16px 0 8px;
-  color: #666;
-}
-
-.no-data p {
-  color: #999;
-  margin-bottom: 20px;
-}
-
-@media (max-width: 768px) {
-  .vendor-header {
+  .vendor-detail-wrap {
+    display: flex;
     flex-direction: column;
+    gap: 20px;
   }
-  
-  .vendor-actions {
+
+  .vendor-header-card {
+    margin-bottom: 20px;
+  }
+
+  .vendor-header {
+    display: flex;
+    align-items: flex-start;
+    gap: 20px;
+  }
+
+  .vendor-logo-section {
+    flex-shrink: 0;
+  }
+
+  .vendor-logo-large {
+    border: 1px solid #e0e0e0;
+  }
+
+  .no-logo-large {
+    background: #f5f5f5;
+    color: #666;
+    font-weight: bold;
+    font-size: 32px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
     width: 100%;
+    height: 100%;
+  }
+
+  .vendor-info-section {
+    flex: 1;
+  }
+
+  .vendor-name {
+    font-size: 28px;
+    font-weight: bold;
+    margin-bottom: 12px;
+  }
+
+  .vendor-meta {
+    margin-bottom: 16px;
+  }
+
+  .vendor-description {
+    color: #666;
+    line-height: 1.6;
+    margin: 0;
+  }
+
+  .vendor-actions {
+    flex-shrink: 0;
+  }
+
+  .breadcrumb-link {
+    cursor: pointer;
+    color: #1976d2;
+  }
+
+  .breadcrumb-link:hover {
+    text-decoration: underline;
+  }
+
+  .detail-tabs-card {
+    margin-top: 20px;
+  }
+
+  .info-section,
+  .contact-section,
+  .products-section,
+  .partnership-section {
+    padding: 20px 0;
+  }
+
+  .info-item {
+    margin-bottom: 24px;
+  }
+
+  .info-item h3 {
+    font-size: 16px;
+    font-weight: 600;
+    margin-bottom: 8px;
+    color: #333;
+  }
+
+  .info-item p {
+    font-size: 14px;
+    color: #666;
+    margin: 0;
+  }
+
+  .contact-card {
+    height: 100%;
+  }
+
+  .contact-item {
+    margin-bottom: 12px;
+  }
+
+  .contact-item strong {
+    display: inline-block;
+    width: 80px;
+    color: #333;
+  }
+
+  .contact-item a {
+    color: #1976d2;
+    text-decoration: none;
+  }
+
+  .contact-item a:hover {
+    text-decoration: underline;
+  }
+
+  .section-header {
+    margin-bottom: 20px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #e0e0e0;
+  }
+
+  .section-header h3 {
+    font-size: 18px;
+    font-weight: 600;
+    margin: 0;
+  }
+
+  .product-card {
+    height: 100%;
+  }
+
+  .product-price {
+    font-weight: bold;
+    color: #1976d2;
+    margin-top: 8px;
+  }
+
+  .rating {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .rating-text {
+    font-size: 14px;
+    color: #666;
+  }
+
+  .business-areas {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
+  .loading-wrap,
+  .error-wrap,
+  .no-data-wrap {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+  }
+
+  .no-data {
+    text-align: center;
+  }
+
+  .no-data h3 {
+    margin: 16px 0 8px;
+    color: #666;
+  }
+
+  .no-data p {
+    color: #999;
+    margin-bottom: 20px;
+  }
+
+  @media (max-width: 768px) {
+    .vendor-header {
+      flex-direction: column;
+    }
+
+    .vendor-actions {
+      width: 100%;
+    }
   }
-}
-</style>
+</style>

+ 1121 - 0
pages/view/vendor/dashboard/influencer-requests.vue

@@ -0,0 +1,1121 @@
+<template>
+  <div>
+    <div class="inner--headers">
+      <h2>{{ pageId }}</h2>
+      <div class="bread--crumbs--wrap">
+        <span>홈</span>
+        <span>벤더 대시보드</span>
+        <span>{{ pageId }}</span>
+      </div>
+    </div>
+
+    <!-- 통계 카드 -->
+    <div class="stats--cards--wrap">
+      <div class="stats--card">
+        <div class="stats--icon pending">
+          <v-icon>mdi-clock-outline</v-icon>
+        </div>
+        <div class="stats--content">
+          <h3>{{ stats.pending || 0 }}</h3>
+          <p>대기 중인 승인요청</p>
+        </div>
+      </div>
+      <div class="stats--card">
+        <div class="stats--icon approved">
+          <v-icon>mdi-check-circle</v-icon>
+        </div>
+        <div class="stats--content">
+          <h3>{{ stats.approved || 0 }}</h3>
+          <p>승인 완료</p>
+        </div>
+      </div>
+      <div class="stats--card">
+        <div class="stats--icon rejected">
+          <v-icon>mdi-close-circle</v-icon>
+        </div>
+        <div class="stats--content">
+          <h3>{{ stats.rejected || 0 }}</h3>
+          <p>거부</p>
+        </div>
+      </div>
+      <div class="stats--card">
+        <div class="stats--icon total">
+          <v-icon>mdi-account-group</v-icon>
+        </div>
+        <div class="stats--content">
+          <h3>{{ stats.total || 0 }}</h3>
+          <p>총 요청 수</p>
+        </div>
+      </div>
+    </div>
+
+    <!-- 필터 및 검색 -->
+    <div class="search--modules type2">
+      <div class="search--inner">
+        <div class="form--cont--filter">
+          <v-select
+            v-model="searchFilter.status"
+            :items="statusOptions"
+            variant="outlined"
+            class="custom-select"
+            label="상태"
+            clearable
+          >
+          </v-select>
+        </div>
+        <div class="form--cont--filter">
+          <v-select
+            v-model="searchFilter.category"
+            :items="categoryOptions"
+            variant="outlined"
+            class="custom-select"
+            label="인플루언서 카테고리"
+            clearable
+          >
+          </v-select>
+        </div>
+        <div class="form--cont--text">
+          <v-text-field
+            v-model="searchFilter.keyword"
+            class="custom-input mini"
+            style="width: 100%"
+            placeholder="인플루언서명을 입력하세요"
+            @keyup.enter="handleSearch"
+          ></v-text-field>
+        </div>
+      </div>
+      <v-btn
+        class="custom-btn btn-blue mini sch--btn"
+        @click="handleSearch"
+        :loading="loading"
+      >
+        검색
+      </v-btn>
+    </div>
+
+    <!-- 인플루언서 승인 요청 목록 -->
+    <div class="data--list--wrap">
+      <div class="btn--actions--wrap">
+        <div class="left--sections">
+          <span class="result-count">
+            총 {{ pagination.totalCount || 0 }}개의 승인요청
+          </span>
+        </div>
+        <div class="right--sections">
+          <v-select
+            v-model="sortOption"
+            :items="sortOptions"
+            variant="outlined"
+            class="custom-select mini"
+            @update:model-value="handleSort"
+          >
+          </v-select>
+        </div>
+      </div>
+
+      <!-- 로딩 상태 -->
+      <div v-if="loading" class="loading-wrap">
+        <v-progress-circular indeterminate color="primary"></v-progress-circular>
+        <p>승인요청을 불러오고 있습니다...</p>
+      </div>
+
+      <!-- 에러 상태 -->
+      <div v-else-if="error" class="error-wrap">
+        <v-alert type="error" dismissible @click:close="error = null">
+          {{ error }}
+        </v-alert>
+      </div>
+
+      <!-- 승인요청 리스트 -->
+      <div v-else-if="requests.length > 0" class="requests--list--wrap">
+        <div class="requests--grid">
+          <div
+            v-for="request in requests"
+            :key="request.SEQ"
+            class="request--card"
+            :class="getRequestStatusClass(request.STATUS)"
+          >
+            <!-- 카드 헤더 -->
+            <div class="request--card--header">
+              <div class="influencer--info">
+                <div class="influencer--avatar">
+                  <v-img
+                    v-if="request.influencerAvatar"
+                    :src="request.influencerAvatar"
+                    :alt="request.influencerNickname + ' 프로필'"
+                    width="50"
+                    height="50"
+                  ></v-img>
+                  <div v-else class="no-avatar">
+                    {{ request.influencerNickname?.charAt(0) || "U" }}
+                  </div>
+                </div>
+                <div class="influencer--details">
+                  <div class="influencer--header">
+                    <h4>{{ request.influencerNickname || request.influencerName }}</h4>
+                    <p class="influencer--category">
+                      {{ getCategoryText(request.influencerCategory) }}
+                    </p>
+                  </div>
+
+                  <div class="influencer--contact">
+                    <p v-if="request.influencerEmail" class="contact--item">
+                      <v-icon size="small">mdi-email</v-icon>
+                      {{ request.influencerEmail }}
+                    </p>
+                    <p v-if="request.influencerPhone" class="contact--item">
+                      <v-icon size="small">mdi-phone</v-icon>
+                      {{ request.influencerPhone }}
+                    </p>
+                    <p v-if="request.influencerRegion" class="contact--item">
+                      <v-icon size="small">mdi-map-marker</v-icon>
+                      {{ request.influencerRegion }}
+                    </p>
+                  </div>
+
+                  <div class="influencer--meta">
+                    <span v-if="request.followerCount" class="meta--item">
+                      <v-icon size="small">mdi-account-group</v-icon>
+                      {{ formatNumber(request.followerCount) }} 팔로워
+                    </span>
+                    <span v-if="request.avgViews" class="meta--item">
+                      <v-icon size="small">mdi-eye</v-icon>
+                      평균 {{ formatNumber(request.avgViews) }} 조회
+                    </span>
+                    <span v-if="request.engagementRate" class="meta--item">
+                      <v-icon size="small">mdi-chart-line</v-icon>
+                      참여율 {{ request.engagementRate }}%
+                    </span>
+                  </div>
+
+                  <div v-if="request.influencerDescription" class="influencer--description">
+                    <p>{{ request.influencerDescription }}</p>
+                  </div>
+
+                  <div v-if="request.influencerSnsChannels" class="influencer--sns">
+                    <div v-for="(channel, index) in parseSnsChannels(request.influencerSnsChannels)" 
+                         :key="index" 
+                         class="sns--item">
+                      <v-icon size="small">{{ getSnsIcon(channel.platform) }}</v-icon>
+                      {{ channel.handle }}
+                    </div>
+                  </div>
+                </div>
+              </div>
+              <div class="request--status">
+                <v-chip :color="getStatusColor(request.STATUS)" size="small">
+                  {{ getStatusText(request.STATUS) }}
+                </v-chip>
+                <p class="request--date">{{ formatDate(request.REQUEST_DATE) }}</p>
+              </div>
+            </div>
+
+            <!-- 카드 바디 -->
+            <div class="request--card--body">
+              <div v-if="request.REQUEST_MESSAGE" class="request--message">
+                <h5>요청 메시지</h5>
+                <p>"{{ request.REQUEST_MESSAGE }}"</p>
+              </div>
+
+              <div
+                v-if="request.COMMISSION_RATE || request.SPECIAL_CONDITIONS"
+                class="request--conditions"
+              >
+                <h5>희망 조건</h5>
+                <div v-if="request.COMMISSION_RATE" class="condition--item">
+                  <span class="condition--label">희망 수수료율:</span>
+                  <span class="condition--value">{{ request.COMMISSION_RATE }}%</span>
+                </div>
+                <div v-if="request.SPECIAL_CONDITIONS" class="condition--item">
+                  <span class="condition--label">특별 조건:</span>
+                  <span class="condition--value">{{ request.SPECIAL_CONDITIONS }}</span>
+                </div>
+              </div>
+
+              <div v-if="request.STATUS === 'PENDING'" class="expire--info">
+                <v-icon size="16" color="warning">mdi-clock-alert</v-icon>
+                <span>만료일: {{ formatDate(request.EXPIRED_DATE) }}</span>
+              </div>
+            </div>
+
+            <!-- 카드 푸터 (액션 버튼) -->
+            <div class="request--card--footer">
+              <div class="card--actions">
+                <v-btn
+                  class="custom-btn mini btn-outline"
+                  @click="viewInfluencerDetail(request.INFLUENCER_SEQ)"
+                >
+                  프로필 보기
+                </v-btn>
+
+                <div v-if="request.STATUS === 'PENDING'" class="approval--actions">
+                  <v-btn
+                    class="custom-btn mini btn-red"
+                    @click="handleReject(request)"
+                    :loading="processing"
+                  >
+                    거부
+                  </v-btn>
+                  <v-btn
+                    class="custom-btn mini btn-blue"
+                    @click="handleApprove(request)"
+                    :loading="processing"
+                  >
+                    승인
+                  </v-btn>
+                </div>
+
+                <v-btn
+                  v-else
+                  class="custom-btn mini btn-outline"
+                  @click="viewRequestHistory(request.SEQ)"
+                >
+                  이력보기
+                </v-btn>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 페이지네이션 -->
+        <div class="pagination-wrap" v-if="pagination.totalPages > 1">
+          <v-pagination
+            v-model="currentPage"
+            :length="pagination.totalPages"
+            :total-visible="7"
+            @update:model-value="handlePageChange"
+          ></v-pagination>
+        </div>
+      </div>
+
+      <!-- 검색 결과 없음 -->
+      <div v-else class="no-data-wrap">
+        <div class="no-data">
+          <v-icon size="64" color="grey-lighten-1">mdi-account-search</v-icon>
+          <h3>승인요청이 없습니다</h3>
+          <p>아직 인플루언서로부터 승인요청이 없습니다</p>
+        </div>
+      </div>
+    </div>
+
+    <!-- 승인 확인 모달 -->
+    <v-dialog v-model="approveModal.show" max-width="500px">
+      <v-card>
+        <v-card-title class="text-h5 text-success">
+          <v-icon left>mdi-check-circle</v-icon>
+          승인 확인
+        </v-card-title>
+        <v-card-text>
+          <div class="approve--content">
+            <div class="influencer--summary">
+              <div class="influencer--avatar--small">
+                <v-img
+                  v-if="approveModal.request?.influencerAvatar"
+                  :src="approveModal.request.influencerAvatar"
+                  width="40"
+                  height="40"
+                ></v-img>
+                <div v-else class="no-avatar--small">
+                  {{ approveModal.request?.influencerNickname?.charAt(0) || "U" }}
+                </div>
+              </div>
+              <div>
+                <h4>{{ approveModal.request?.influencerNickname }}</h4>
+                <p>{{ getCategoryText(approveModal.request?.influencerCategory) }}</p>
+              </div>
+            </div>
+
+            <p>이 인플루언서의 승인요청을 승인하시겠습니까?</p>
+
+            <v-textarea
+              v-model="approveModal.approveMessage"
+              label="승인 메시지 (선택사항)"
+              placeholder="인플루언서에게 전달할 메시지를 입력해주세요..."
+              rows="3"
+              counter="300"
+              maxlength="300"
+              class="mt-4"
+            ></v-textarea>
+          </div>
+        </v-card-text>
+        <v-card-actions>
+          <v-spacer></v-spacer>
+          <v-btn color="grey" variant="text" @click="closeApproveModal">취소</v-btn>
+          <v-btn color="success" @click="confirmApprove" :loading="processing">
+            승인하기
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+
+    <!-- 거부 확인 모달 -->
+    <v-dialog v-model="rejectModal.show" max-width="500px">
+      <v-card>
+        <v-card-title class="text-h5 text-error">
+          <v-icon left>mdi-close-circle</v-icon>
+          거부 확인
+        </v-card-title>
+        <v-card-text>
+          <div class="reject--content">
+            <div class="influencer--summary">
+              <div class="influencer--avatar--small">
+                <v-img
+                  v-if="rejectModal.request?.influencerAvatar"
+                  :src="rejectModal.request.influencerAvatar"
+                  width="40"
+                  height="40"
+                ></v-img>
+                <div v-else class="no-avatar--small">
+                  {{ rejectModal.request?.influencerNickname?.charAt(0) || "U" }}
+                </div>
+              </div>
+              <div>
+                <h4>{{ rejectModal.request?.influencerNickname }}</h4>
+                <p>{{ getCategoryText(rejectModal.request?.influencerCategory) }}</p>
+              </div>
+            </div>
+
+            <p>이 인플루언서의 승인요청을 거부하시겠습니까?</p>
+
+            <v-textarea
+              v-model="rejectModal.rejectReason"
+              label="거부 사유"
+              placeholder="거부 사유를 입력해주세요..."
+              rows="4"
+              counter="500"
+              maxlength="500"
+              class="mt-4"
+              required
+            ></v-textarea>
+          </div>
+        </v-card-text>
+        <v-card-actions>
+          <v-spacer></v-spacer>
+          <v-btn color="grey" variant="text" @click="closeRejectModal">취소</v-btn>
+          <v-btn color="error" @click="confirmReject" :loading="processing">
+            거부하기
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, onMounted, computed } from "vue";
+  import { useRouter } from "vue-router";
+
+  /************************************************************************
+  |    레이아웃
+  ************************************************************************/
+  definePageMeta({
+    layout: "default",
+  });
+
+  /************************************************************************
+  |    스토어 & 라우터
+  ************************************************************************/
+  const router = useRouter();
+  const { $toast } = useNuxtApp();
+
+  /************************************************************************
+  |    반응형 데이터
+  ************************************************************************/
+  const pageId = ref("인플루언서 승인요청 관리");
+  const loading = ref(false);
+  const processing = ref(false);
+  const error = ref(null);
+  const currentPage = ref(1);
+
+  // 검색 필터
+  const searchFilter = ref({
+    keyword: "",
+    status: "",
+    category: "",
+  });
+
+  // 정렬 옵션
+  const sortOption = ref("latest");
+  const sortOptions = ref([
+    { title: "최신순", value: "latest" },
+    { title: "오래된순", value: "oldest" },
+    { title: "마감임박순", value: "expiring" },
+  ]);
+
+  // 상태 옵션
+  const statusOptions = ref([
+    { title: "전체", value: "" },
+    { title: "대기중", value: "PENDING" },
+    { title: "승인완료", value: "APPROVED" },
+    { title: "거부됨", value: "REJECTED" },
+  ]);
+
+  // 카테고리 옵션
+  const categoryOptions = ref([
+    { title: "전체", value: "" },
+    { title: "패션·뷰티", value: "FASHION_BEAUTY" },
+    { title: "식품·건강", value: "FOOD_HEALTH" },
+    { title: "라이프스타일", value: "LIFESTYLE" },
+    { title: "테크·가전", value: "TECH_ELECTRONICS" },
+    { title: "스포츠·레저", value: "SPORTS_LEISURE" },
+    { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" },
+  ]);
+
+  // 데이터
+  const requests = ref([]);
+  const stats = ref({
+    pending: 0,
+    approved: 0,
+    rejected: 0,
+    total: 0,
+  });
+  const pagination = ref({
+    currentPage: 1,
+    totalPages: 1,
+    totalCount: 0,
+    pageSize: 12,
+  });
+
+  // 승인 모달
+  const approveModal = ref({
+    show: false,
+    request: null,
+    approveMessage: "",
+  });
+
+  // 거부 모달
+  const rejectModal = ref({
+    show: false,
+    request: null,
+    rejectReason: "",
+  });
+
+  /************************************************************************
+  |    computed
+  ************************************************************************/
+  const currentUser = computed(() => {
+    return JSON.parse(localStorage.getItem("authStore"))?.auth || {};
+  });
+
+  /************************************************************************
+  |    메서드
+  ************************************************************************/
+  const handleSearch = async () => {
+    currentPage.value = 1;
+    await loadRequests();
+  };
+
+  const handlePageChange = async (page) => {
+    currentPage.value = page;
+    await loadRequests();
+  };
+
+  const handleSort = async () => {
+    currentPage.value = 1;
+    await loadRequests();
+  };
+
+  const loadRequests = async () => {
+    try {
+      loading.value = true;
+      error.value = null;
+
+      const params = {
+        vendorSeq: currentUser.value.seq,
+        keyword: searchFilter.value.keyword,
+        status: searchFilter.value.status,
+        category: searchFilter.value.category,
+        sortBy: sortOption.value,
+        page: currentPage.value,
+        size: pagination.value.pageSize,
+      };
+
+      useAxios()
+        .post("/api/vendor-influencer/requests", params)
+        .then((res) => {
+          if (res.data.success) {
+            requests.value = res.data.data.items;
+            pagination.value = res.data.data.pagination;
+            stats.value = res.data.data.stats || stats.value;
+          } else {
+            error.value =
+              res.data.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
+          }
+        })
+        .catch((err) => {
+          error.value = err.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
+        })
+        .finally(() => {
+          loading.value = false;
+        });
+    } catch (err) {
+      error.value = err.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
+      loading.value = false;
+    }
+  };
+
+  const handleApprove = (request) => {
+    approveModal.value = {
+      show: true,
+      request: request,
+      approveMessage: "",
+    };
+  };
+
+  const closeApproveModal = () => {
+    approveModal.value = {
+      show: false,
+      request: null,
+      approveMessage: "",
+    };
+  };
+
+  const confirmApprove = async () => {
+    try {
+      processing.value = true;
+
+      const params = {
+        mappingSeq: approveModal.value.request.SEQ,
+        action: "APPROVE",
+        processedBy: currentUser.value.seq,
+        responseMessage: approveModal.value.approveMessage,
+      };
+
+      useAxios()
+        .post("/api/vendor-influencer/process", params)
+        .then((res) => {
+          if (res.data.success) {
+            $toast.success("승인요청이 승인되었습니다.");
+            closeApproveModal();
+            loadRequests();
+          } else {
+            $toast.error(res.data.message || "승인 처리 중 오류가 발생했습니다.");
+          }
+        })
+        .catch((err) => {
+          $toast.error(err.message || "승인 처리 중 오류가 발생했습니다.");
+        })
+        .finally(() => {
+          processing.value = false;
+        });
+    } catch (err) {
+      $toast.error(err.message || "승인 처리 중 오류가 발생했습니다.");
+      processing.value = false;
+    }
+  };
+
+  const handleReject = (request) => {
+    rejectModal.value = {
+      show: true,
+      request: request,
+      rejectReason: "",
+    };
+  };
+
+  const closeRejectModal = () => {
+    rejectModal.value = {
+      show: false,
+      request: null,
+      rejectReason: "",
+    };
+  };
+
+  const confirmReject = async () => {
+    if (!rejectModal.value.rejectReason.trim()) {
+      $toast.error("거부 사유를 입력해주세요.");
+      return;
+    }
+
+    try {
+      processing.value = true;
+
+      const params = {
+        mappingSeq: rejectModal.value.request.SEQ,
+        action: "REJECT",
+        processedBy: currentUser.value.seq,
+        responseMessage: rejectModal.value.rejectReason,
+      };
+
+      useAxios()
+        .post("/api/vendor-influencer/process", params)
+        .then((res) => {
+          if (res.data.success) {
+            $toast.success("승인요청이 거부되었습니다.");
+            closeRejectModal();
+            loadRequests();
+          } else {
+            $toast.error(res.data.message || "거부 처리 중 오류가 발생했습니다.");
+          }
+        })
+        .catch((err) => {
+          $toast.error(err.message || "거부 처리 중 오류가 발생했습니다.");
+        })
+        .finally(() => {
+          processing.value = false;
+        });
+    } catch (err) {
+      $toast.error(err.message || "거부 처리 중 오류가 발생했습니다.");
+      processing.value = false;
+    }
+  };
+
+  const viewInfluencerDetail = (influencerSeq) => {
+    router.push(`/view/influencer/${influencerSeq}`);
+  };
+
+  const viewRequestHistory = (requestSeq) => {
+    router.push(`/view/vendor/request-history/${requestSeq}`);
+  };
+
+  // 유틸리티 함수들
+  const getCategoryText = (category) => {
+    const categoryMap = {
+      FASHION_BEAUTY: "패션·뷰티",
+      FOOD_HEALTH: "식품·건강",
+      LIFESTYLE: "라이프스타일",
+      TECH_ELECTRONICS: "테크·가전",
+      SPORTS_LEISURE: "스포츠·레저",
+      CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
+    };
+    return categoryMap[category] || category || "기타";
+  };
+
+  const getStatusText = (status) => {
+    const statusMap = {
+      PENDING: "대기중",
+      APPROVED: "승인완료",
+      REJECTED: "거절됨",
+      EXPIRED: "만료됨",
+    };
+    return statusMap[status] || status || "알 수 없음";
+  };
+
+  const getStatusColor = (status) => {
+    const colorMap = {
+      PENDING: "orange",
+      APPROVED: "success",
+      REJECTED: "error",
+      EXPIRED: "grey",
+    };
+    return colorMap[status] || "grey";
+  };
+
+  const getRequestStatusClass = (status) => {
+    return `request-status-${status?.toLowerCase() || "unknown"}`;
+  };
+
+  const formatDate = (dateString) => {
+    return new Date(dateString).toLocaleDateString("ko-KR");
+  };
+
+  const formatNumber = (num) => {
+    if (!num) return "0";
+    if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
+    if (num >= 1000) return (num / 1000).toFixed(1) + "K";
+    return num.toString();
+  };
+
+  const parseSnsChannels = (snsChannels) => {
+    try {
+      return JSON.parse(snsChannels);
+    } catch (e) {
+      return [];
+    }
+  };
+
+  const getSnsIcon = (platform) => {
+    const iconMap = {
+      instagram: 'mdi-instagram',
+      youtube: 'mdi-youtube',
+      tiktok: 'mdi-music-note',
+      blog: 'mdi-post',
+      facebook: 'mdi-facebook',
+      twitter: 'mdi-twitter'
+    };
+    return iconMap[platform.toLowerCase()] || 'mdi-link';
+  };
+
+  /************************************************************************
+  |    라이프사이클
+  ************************************************************************/
+  onMounted(async () => {
+    await loadRequests();
+  });
+</script>
+
+<style scoped>
+  .stats--cards--wrap {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+    gap: 20px;
+    margin-bottom: 30px;
+  }
+
+  .stats--card {
+    background: white;
+    border-radius: 12px;
+    padding: 20px;
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+
+  .stats--icon {
+    width: 50px;
+    height: 50px;
+    border-radius: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+  }
+
+  .stats--icon.pending {
+    background: #ff9800;
+  }
+
+  .stats--icon.approved {
+    background: #4caf50;
+  }
+
+  .stats--icon.rejected {
+    background: #f44336;
+  }
+
+  .stats--icon.total {
+    background: #2196f3;
+  }
+
+  .stats--content h3 {
+    margin: 0;
+    font-size: 24px;
+    font-weight: 700;
+    color: #333;
+  }
+
+  .stats--content p {
+    margin: 0;
+    font-size: 14px;
+    color: #666;
+  }
+
+  .requests--list--wrap {
+    margin-top: 20px;
+  }
+
+  .requests--grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
+    gap: 20px;
+    margin-bottom: 20px;
+  }
+
+  .request--card {
+    background: white;
+    border-radius: 12px;
+    padding: 20px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    transition: transform 0.2s, box-shadow 0.2s;
+    border-left: 4px solid #e0e0e0;
+  }
+
+  .request--card:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+  }
+
+  .request--card.request-status-pending {
+    border-left-color: #ff9800;
+  }
+
+  .request--card.request-status-approved {
+    border-left-color: #4caf50;
+  }
+
+  .request--card.request-status-rejected {
+    border-left-color: #f44336;
+  }
+
+  .request--card--header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    margin-bottom: 16px;
+  }
+
+  .influencer--info {
+    display: flex;
+    gap: 12px;
+    flex: 1;
+  }
+
+  .influencer--avatar {
+    width: 50px;
+    height: 50px;
+    border-radius: 50%;
+    overflow: hidden;
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: #f5f5f5;
+  }
+
+  .no-avatar {
+    font-size: 20px;
+    font-weight: bold;
+    color: #666;
+  }
+
+  .influencer--details h4 {
+    margin: 0 0 4px 0;
+    font-size: 16px;
+    font-weight: 600;
+  }
+
+  .influencer--category {
+    color: #666;
+    font-size: 14px;
+    margin: 0 0 8px 0;
+  }
+
+  .influencer--meta {
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+  }
+
+  .influencer--meta span {
+    font-size: 12px;
+    color: #888;
+  }
+
+  .request--status {
+    text-align: right;
+    flex-shrink: 0;
+  }
+
+  .request--date {
+    margin: 8px 0 0;
+    font-size: 12px;
+    color: #999;
+  }
+
+  .request--card--body {
+    margin-bottom: 16px;
+  }
+
+  .request--message,
+  .request--conditions {
+    margin-bottom: 12px;
+  }
+
+  .request--message h5,
+  .request--conditions h5 {
+    margin: 0 0 8px 0;
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+  }
+
+  .request--message p {
+    margin: 0;
+    font-size: 14px;
+    color: #666;
+    font-style: italic;
+  }
+
+  .condition--item {
+    display: flex;
+    gap: 8px;
+    margin-bottom: 4px;
+  }
+
+  .condition--label {
+    font-size: 13px;
+    color: #666;
+    min-width: 80px;
+  }
+
+  .condition--value {
+    font-size: 13px;
+    color: #333;
+    font-weight: 500;
+  }
+
+  .expire--info {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 12px;
+    color: #ff9800;
+    background: #fff8e1;
+    padding: 6px 10px;
+    border-radius: 6px;
+  }
+
+  .request--card--footer {
+    border-top: 1px solid #f0f0f0;
+    padding-top: 16px;
+  }
+
+  .card--actions {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .approval--actions {
+    display: flex;
+    gap: 8px;
+  }
+
+  .loading-wrap,
+  .error-wrap,
+  .no-data-wrap {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+  }
+
+  .no-data {
+    text-align: center;
+  }
+
+  .no-data h3 {
+    margin: 16px 0 8px;
+    color: #666;
+  }
+
+  .no-data p {
+    color: #999;
+  }
+
+  .pagination-wrap {
+    display: flex;
+    justify-content: center;
+    margin-top: 20px;
+  }
+
+  .approve--content,
+  .reject--content {
+    padding: 8px 0;
+  }
+
+  .influencer--summary {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px;
+    background: #f8f9fa;
+    border-radius: 8px;
+    margin-bottom: 16px;
+  }
+
+  .influencer--avatar--small {
+    width: 40px;
+    height: 40px;
+    border-radius: 50%;
+    overflow: hidden;
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: #f5f5f5;
+  }
+
+  .no-avatar--small {
+    font-size: 16px;
+    font-weight: bold;
+    color: #666;
+  }
+
+  .result-count {
+    font-size: 14px;
+    color: #666;
+    font-weight: 500;
+  }
+
+  .influencer--contact {
+    margin: 4px 0;
+  }
+
+  .contact--item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 13px;
+    color: #666;
+    margin: 2px 0;
+  }
+
+  .contact--item .v-icon {
+    color: #999;
+  }
+
+  .influencer--header {
+    margin-bottom: 8px;
+  }
+
+  .influencer--contact {
+    margin: 8px 0;
+    padding: 8px;
+    background: #f8f9fa;
+    border-radius: 6px;
+  }
+
+  .contact--item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 13px;
+    color: #666;
+    margin: 4px 0;
+  }
+
+  .influencer--meta {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+    margin: 8px 0;
+  }
+
+  .meta--item {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    font-size: 13px;
+    color: #555;
+    background: #f0f0f0;
+    padding: 4px 8px;
+    border-radius: 4px;
+  }
+
+  .influencer--description {
+    margin: 8px 0;
+    font-size: 13px;
+    color: #666;
+    line-height: 1.4;
+  }
+
+  .influencer--sns {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    margin-top: 8px;
+  }
+
+  .sns--item {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    font-size: 12px;
+    color: #555;
+    background: #eef2ff;
+    padding: 4px 8px;
+    border-radius: 4px;
+  }
+</style>

+ 941 - 0
pages/view/vendor/search.vue

@@ -0,0 +1,941 @@
+<template>
+  <div>
+    <div class="inner--headers">
+      <h2>{{ pageId }}</h2>
+      <div class="bread--crumbs--wrap">
+        <span>홈</span>
+        <span>{{ pageId }}</span>
+      </div>
+    </div>
+
+    <!-- 검색 및 필터 영역 -->
+    <div class="search--modules type2">
+      <div class="search--inner">
+        <div class="form--cont--filter">
+          <v-select
+            v-model="searchFilter.category"
+            :items="categoryOptions"
+            variant="outlined"
+            class="custom-select"
+            label="카테고리"
+            clearable
+          >
+          </v-select>
+        </div>
+        <div class="form--cont--filter">
+          <v-select
+            v-model="searchFilter.region"
+            :items="regionOptions"
+            variant="outlined"
+            class="custom-select"
+            label="지역"
+            clearable
+          >
+          </v-select>
+        </div>
+        <div class="form--cont--text">
+          <v-text-field
+            v-model="searchFilter.keyword"
+            class="custom-input mini"
+            style="width: 100%"
+            placeholder="벤더사명을 입력하세요"
+            @keyup.enter="handleSearch"
+          ></v-text-field>
+        </div>
+      </div>
+      <v-btn
+        class="custom-btn btn-blue mini sch--btn"
+        @click="handleSearch"
+        :loading="loading"
+      >
+        검색
+      </v-btn>
+    </div>
+
+    <!-- 내 승인 요청 현황 -->
+    <div class="data--list--wrap mb-4">
+      <div class="section--header">
+        <h3>내 승인 요청 현황</h3>
+        <v-btn
+          class="custom-btn mini btn-outline"
+          @click="showMyRequests = !showMyRequests"
+        >
+          {{ showMyRequests ? "숨기기" : "보기" }}
+        </v-btn>
+      </div>
+
+      <div v-show="showMyRequests" class="my--requests--wrap">
+        <div v-if="myRequests.length === 0" class="no-data">
+          <p>진행 중인 승인 요청이 없습니다.</p>
+        </div>
+        <div v-else class="request--cards">
+          <div
+            v-for="request in myRequests"
+            :key="request.SEQ"
+            class="request--card"
+            :class="getStatusClass(request.STATUS)"
+          >
+            <div class="card--header">
+              <h4>{{ request.vendorName }}</h4>
+              <v-chip :color="getStatusColor(request.STATUS)" size="small">
+                {{ getStatusText(request.STATUS) }}
+              </v-chip>
+            </div>
+            <div class="card--content">
+              <p class="request--date">요청일: {{ formatDate(request.REQUEST_DATE) }}</p>
+              <p v-if="request.STATUS === 'PENDING'" class="expire--date">
+                만료일: {{ formatDate(request.EXPIRED_DATE) }}
+              </p>
+              <p v-if="request.REQUEST_MESSAGE" class="request--message">
+                "{{ request.REQUEST_MESSAGE }}"
+              </p>
+            </div>
+            <div class="card--actions">
+              <v-btn
+                v-if="request.STATUS === 'PENDING'"
+                class="custom-btn mini btn-outline-red"
+                @click="cancelRequest(request.SEQ)"
+                size="small"
+              >
+                취소
+              </v-btn>
+              <v-btn
+                class="custom-btn mini btn-outline"
+                @click="viewRequestDetail(request.SEQ)"
+                size="small"
+              >
+                상세보기
+              </v-btn>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 벤더사 검색 결과 -->
+    <div class="data--list--wrap">
+      <div class="btn--actions--wrap">
+        <div class="left--sections">
+          <span class="result-count">
+            총 {{ pagination.totalCount || 0 }}개의 벤더사
+          </span>
+        </div>
+        <div class="right--sections">
+          <v-select
+            v-model="sortOption"
+            :items="sortOptions"
+            variant="outlined"
+            class="custom-select mini"
+            @update:model-value="handleSort"
+          >
+          </v-select>
+        </div>
+      </div>
+
+      <!-- 로딩 상태 -->
+      <div v-if="loading" class="loading-wrap">
+        <v-progress-circular indeterminate color="primary"></v-progress-circular>
+        <p>벤더사를 검색하고 있습니다...</p>
+      </div>
+
+      <!-- 에러 상태 -->
+      <div v-else-if="error" class="error-wrap">
+        <v-alert type="error" dismissible @click:close="error = null">
+          {{ error }}
+        </v-alert>
+      </div>
+
+      <!-- 벤더사 리스트 -->
+      <div v-else-if="vendors.length > 0" class="vendor--search--wrap">
+        <div class="vendor--grid">
+          <div v-for="vendor in vendors" :key="vendor.SEQ" class="vendor--card">
+            <div class="vendor--card--header">
+              <div class="vendor--logo">
+                <v-img
+                  v-if="vendor.LOGO"
+                  :src="vendor.LOGO"
+                  :alt="vendor.COMPANY_NAME + ' 로고'"
+                  width="60"
+                  height="60"
+                ></v-img>
+                <div v-else class="no-logo">
+                  {{ vendor.COMPANY_NAME?.charAt(0) || "V" }}
+                </div>
+              </div>
+              <div class="vendor--info">
+                <h3>{{ vendor.COMPANY_NAME }}</h3>
+                <p class="vendor--category">{{ getCategoryText(vendor.CATEGORY) }}</p>
+                <div class="vendor--meta">
+                  <span v-if="vendor.REGION">📍 {{ vendor.REGION }}</span>
+                  <span v-if="vendor.PARTNERSHIP_COUNT"
+                    >🤝 {{ vendor.PARTNERSHIP_COUNT }}개 파트너십</span
+                  >
+                </div>
+              </div>
+            </div>
+
+            <div class="vendor--card--body">
+              <p v-if="vendor.DESCRIPTION" class="vendor--description">
+                {{ vendor.DESCRIPTION }}
+              </p>
+              <div class="vendor--tags" v-if="vendor.TAGS">
+                <v-chip
+                  v-for="tag in vendor.TAGS.split(',')"
+                  :key="tag"
+                  size="small"
+                  variant="outlined"
+                  class="mr-1 mb-1"
+                >
+                  {{ tag.trim() }}
+                </v-chip>
+              </div>
+            </div>
+
+            <div class="vendor--card--footer">
+              <div class="partnership--status">
+                <span
+                  v-if="getPartnershipStatus(vendor.SEQ)"
+                  :class="[
+                    'status-badge',
+                    getPartnershipStatus(vendor.SEQ)?.toLowerCase() || 'unknown',
+                  ]"
+                >
+                  {{ getPartnershipStatusText(vendor.SEQ) }}
+                </span>
+                <span v-else class="status-badge available">신규 파트너십 가능</span>
+              </div>
+              <div class="card--actions">
+                <v-btn
+                  class="custom-btn mini btn-outline mr-2"
+                  @click="viewVendorDetail(vendor.SEQ)"
+                >
+                  상세보기
+                </v-btn>
+                <v-btn
+                  v-if="showRequestButton(vendor.SEQ)"
+                  class="custom-btn mini btn-blue"
+                  @click="openRequestModal(vendor)"
+                >
+                  승인요청
+                </v-btn>
+                <v-chip
+                  v-else-if="getPartnershipStatus(vendor.SEQ) === 'APPROVED'"
+                  color="success"
+                  size="small"
+                >
+                  승인완료
+                </v-chip>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 페이지네이션 -->
+        <div class="pagination-wrap" v-if="pagination.totalPages > 1">
+          <v-pagination
+            v-model="currentPage"
+            :length="pagination.totalPages"
+            :total-visible="7"
+            @update:model-value="handlePageChange"
+          ></v-pagination>
+        </div>
+      </div>
+
+      <!-- 검색 결과 없음 -->
+      <div v-else class="no-data-wrap">
+        <div class="no-data">
+          <v-icon size="64" color="grey-lighten-1">mdi-store-search</v-icon>
+          <h3>검색된 벤더사가 없습니다</h3>
+          <p>다른 검색 조건을 시도해보세요</p>
+        </div>
+      </div>
+    </div>
+
+    <!-- 승인요청 모달 -->
+    <v-dialog v-model="requestModal.show" max-width="600px">
+      <v-card>
+        <v-card-title class="text-h5">
+          {{ requestModal.vendor?.COMPANY_NAME }}에 승인요청
+        </v-card-title>
+        <v-card-text>
+          <div class="request--form">
+            <div class="vendor--summary">
+              <div class="vendor--logo--small">
+                <v-img
+                  v-if="requestModal.vendor?.LOGO"
+                  :src="requestModal.vendor.LOGO"
+                  width="40"
+                  height="40"
+                ></v-img>
+                <div v-else class="no-logo--small">
+                  {{ requestModal.vendor?.COMPANY_NAME?.charAt(0) || "V" }}
+                </div>
+              </div>
+              <div>
+                <h4>{{ requestModal.vendor?.COMPANY_NAME }}</h4>
+                <p>{{ getCategoryText(requestModal.vendor?.CATEGORY) }}</p>
+              </div>
+            </div>
+
+            <v-textarea
+              v-model="requestModal.message"
+              label="요청 메시지"
+              placeholder="벤더사에 전달할 메시지를 작성해주세요..."
+              rows="4"
+              counter="500"
+              maxlength="500"
+              class="mt-4"
+            ></v-textarea>
+
+            <div class="form--section">
+              <h5>희망 조건 (선택사항)</h5>
+              <v-text-field
+                v-model="requestModal.commissionRate"
+                label="희망 수수료율 (%)"
+                type="number"
+                min="0"
+                max="100"
+                step="0.1"
+                class="mt-2"
+              ></v-text-field>
+
+              <v-textarea
+                v-model="requestModal.specialConditions"
+                label="특별 조건"
+                placeholder="기타 협업 조건이나 요청사항을 입력해주세요..."
+                rows="3"
+                class="mt-2"
+              ></v-textarea>
+            </div>
+          </div>
+        </v-card-text>
+        <v-card-actions>
+          <v-spacer></v-spacer>
+          <v-btn color="grey" variant="text" @click="closeRequestModal"> 취소 </v-btn>
+          <v-btn color="primary" @click="submitRequest" :loading="submitting">
+            승인요청
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { ref, onMounted, computed } from "vue";
+  import { useRouter } from "vue-router";
+
+  /************************************************************************
+|    레이아웃
+************************************************************************/
+  definePageMeta({
+    layout: "default",
+  });
+
+  /************************************************************************
+|    스토어 & 라우터
+************************************************************************/
+  const router = useRouter();
+  const { $toast } = useNuxtApp();
+
+  /************************************************************************
+|    반응형 데이터
+************************************************************************/
+  const pageId = ref("벤더사 검색");
+  const loading = ref(false);
+  const submitting = ref(false);
+  const error = ref(null);
+  const currentPage = ref(1);
+  const showMyRequests = ref(false);
+
+  // 검색 필터
+  const searchFilter = ref({
+    keyword: "",
+    category: "",
+    region: "",
+  });
+
+  // 정렬 옵션
+  const sortOption = ref("latest");
+  const sortOptions = ref([
+    { title: "최신순", value: "latest" },
+    { title: "파트너십 많은순", value: "partnership" },
+    { title: "이름순", value: "name" },
+  ]);
+
+  // 카테고리 옵션
+  const categoryOptions = ref([
+    { title: "전체", value: "" },
+    { title: "패션·뷰티", value: "FASHION_BEAUTY" },
+    { title: "식품·건강", value: "FOOD_HEALTH" },
+    { title: "라이프스타일", value: "LIFESTYLE" },
+    { title: "테크·가전", value: "TECH_ELECTRONICS" },
+    { title: "스포츠·레저", value: "SPORTS_LEISURE" },
+    { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" },
+  ]);
+
+  // 지역 옵션
+  const regionOptions = ref([
+    { title: "전체", value: "" },
+    { title: "서울", value: "SEOUL" },
+    { title: "경기", value: "GYEONGGI" },
+    { title: "인천", value: "INCHEON" },
+    { title: "부산", value: "BUSAN" },
+    { title: "대구", value: "DAEGU" },
+    { title: "대전", value: "DAEJEON" },
+    { title: "광주", value: "GWANGJU" },
+    { title: "울산", value: "ULSAN" },
+    { title: "기타", value: "OTHER" },
+  ]);
+
+  // 데이터
+  const vendors = ref([]);
+  const myRequests = ref([]);
+  const pagination = ref({
+    currentPage: 1,
+    totalPages: 1,
+    totalCount: 0,
+    pageSize: 12,
+  });
+
+  // 승인요청 모달
+  const requestModal = ref({
+    show: false,
+    vendor: null,
+    message: "",
+    commissionRate: null,
+    specialConditions: "",
+  });
+
+  /************************************************************************
+|    computed
+************************************************************************/
+  const currentUser = computed(() => {
+    // 실제로는 인증 스토어에서 가져옴
+    return JSON.parse(localStorage.getItem("authStore"))?.auth || {};
+  });
+
+  /************************************************************************
+|    메서드
+************************************************************************/
+  const handleSearch = async () => {
+    currentPage.value = 1;
+    await loadVendors();
+  };
+
+  const handlePageChange = async (page) => {
+    currentPage.value = page;
+    await loadVendors();
+  };
+
+  const handleSort = async () => {
+    currentPage.value = 1;
+    await loadVendors();
+  };
+
+  const loadVendors = async () => {
+    try {
+      loading.value = true;
+      error.value = null;
+
+      const params = {
+        keyword: searchFilter.value.keyword,
+        category: searchFilter.value.category,
+        region: searchFilter.value.region,
+        sortBy: sortOption.value,
+        page: currentPage.value,
+        size: pagination.value.pageSize,
+        influencerSeq: currentUser.value.seq,
+      };
+
+      useAxios()
+        .post("/api/vendor/search", params)
+        .then((res) => {
+          if (res.data.success) {
+            vendors.value = res.data.data.items;
+            pagination.value = res.data.data.pagination;
+          } else {
+            error.value = res.data.message || "벤더사 검색 중 오류가 발생했습니다.";
+          }
+        })
+        .catch((err) => {
+          error.value = err.message || "벤더사 검색 중 오류가 발생했습니다.";
+        })
+        .finally(() => {
+          loading.value = false;
+        });
+    } catch (err) {
+      error.value = err.message || "벤더사 검색 중 오류가 발생했습니다.";
+      loading.value = false;
+    }
+  };
+
+  const loadMyRequests = async () => {
+    try {
+      const params = {
+        influencerSeq: currentUser.value.seq,
+        // status 파라미터 제거하여 모든 상태의 요청을 로드
+      };
+
+      useAxios()
+        .post("/api/vendor-influencer/list", params)
+        .then((res) => {
+          if (res.data.success) {
+            myRequests.value = res.data.data.items;
+          }
+        })
+        .catch((err) => {
+          console.error("내 요청 목록 로드 오류:", err);
+        });
+    } catch (err) {
+      console.error("내 요청 목록 로드 오류:", err);
+    }
+  };
+
+  const openRequestModal = (vendor) => {
+    requestModal.value = {
+      show: true,
+      vendor: vendor,
+      message: "",
+      commissionRate: null,
+      specialConditions: "",
+    };
+  };
+
+  const closeRequestModal = () => {
+    requestModal.value = {
+      show: false,
+      vendor: null,
+      message: "",
+      commissionRate: null,
+      specialConditions: "",
+    };
+  };
+
+  const submitRequest = async () => {
+    try {
+      submitting.value = true;
+
+      const params = {
+        vendorSeq: requestModal.value.vendor.SEQ,
+        influencerSeq: currentUser.value.seq,
+        requestType: "INFLUENCER_REQUEST",
+        requestMessage: requestModal.value.message,
+        requestedBy: currentUser.value.seq,
+        commissionRate: requestModal.value.commissionRate,
+        specialConditions: requestModal.value.specialConditions,
+      };
+
+      useAxios()
+        .post("/api/vendor-influencer/request", params)
+        .then((res) => {
+          if (res.data.success) {
+            $toast.success("승인요청이 성공적으로 전송되었습니다.");
+            closeRequestModal();
+            loadMyRequests();
+            loadVendors();
+          } else {
+            $toast.error(res.data.message || "승인요청 전송 중 오류가 발생했습니다.");
+          }
+        })
+        .catch((err) => {
+          $toast.error(err.message || "승인요청 전송 중 오류가 발생했습니다.");
+        })
+        .finally(() => {
+          submitting.value = false;
+        });
+    } catch (err) {
+      $toast.error(err.message || "승인요청 전송 중 오류가 발생했습니다.");
+      submitting.value = false;
+    }
+  };
+
+  const cancelRequest = async (requestSeq) => {
+    if (!confirm("승인요청을 취소하시겠습니까?")) return;
+
+    try {
+      const params = {
+        mappingSeq: requestSeq,
+        cancelledBy: currentUser.value.SEQ,
+        cancelReason: "사용자에 의한 취소",
+      };
+
+      useAxios()
+        .post("/api/vendor-influencer/cancel", params)
+        .then((res) => {
+          if (res.data.success) {
+            $toast.success("승인요청이 취소되었습니다.");
+            loadMyRequests();
+          } else {
+            $toast.error(res.data.message || "요청 취소 중 오류가 발생했습니다.");
+          }
+        })
+        .catch((err) => {
+          $toast.error(err.message || "요청 취소 중 오류가 발생했습니다.");
+        });
+    } catch (err) {
+      $toast.error(err.message || "요청 취소 중 오류가 발생했습니다.");
+    }
+  };
+
+  const viewVendorDetail = (vendorSeq) => {
+    router.push(`/view/vendor/${vendorSeq}`);
+  };
+
+  const viewRequestDetail = (requestSeq) => {
+    router.push(`/view/vendor/request/${requestSeq}`);
+  };
+
+  // 유틸리티 함수들
+  const getCategoryText = (category) => {
+    const categoryMap = {
+      FASHION_BEAUTY: "패션·뷰티",
+      FOOD_HEALTH: "식품·건강",
+      LIFESTYLE: "라이프스타일",
+      TECH_ELECTRONICS: "테크·가전",
+      SPORTS_LEISURE: "스포츠·레저",
+      CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
+    };
+    return categoryMap[category] || category || "기타";
+  };
+
+  const getStatusText = (status) => {
+    const statusMap = {
+      PENDING: "대기중",
+      APPROVED: "승인완료",
+      REJECTED: "거절됨",
+      CANCELLED: "취소됨",
+    };
+    return statusMap[status] || status || "알 수 없음";
+  };
+
+  const getStatusColor = (status) => {
+    const colorMap = {
+      PENDING: "orange",
+      APPROVED: "success",
+      REJECTED: "error",
+      CANCELLED: "grey",
+    };
+    return colorMap[status] || "grey";
+  };
+
+  const getStatusClass = (status) => {
+    return `status-${status?.toLowerCase() || "unknown"}`;
+  };
+
+  const getPartnershipStatus = (vendorSeq) => {
+    // 현재 인플루언서의 해당 벤더사에 대한 파트너십 상태 확인
+    const request = myRequests.value.find(req => req.VENDOR_SEQ === vendorSeq);
+    return request ? request.STATUS : null;
+  };
+
+  const getPartnershipStatusText = (vendorSeq) => {
+    const status = getPartnershipStatus(vendorSeq);
+    const statusMap = {
+      PENDING: "승인 대기중",
+      APPROVED: "승인 완료",
+      REJECTED: "승인 거절됨",
+      CANCELLED: "요청 취소됨"
+    };
+    return statusMap[status] || "신규 파트너십 가능";
+  };
+
+  // 승인요청 버튼 표시 여부 확인
+  const showRequestButton = (vendorSeq) => {
+    const status = getPartnershipStatus(vendorSeq);
+    return !status || status === 'REJECTED' || status === 'CANCELLED';
+  };
+
+  const formatDate = (dateString) => {
+    return new Date(dateString).toLocaleDateString("ko-KR");
+  };
+
+  /************************************************************************
+|    라이프사이클
+************************************************************************/
+  onMounted(async () => {
+    await Promise.all([loadVendors(), loadMyRequests()]);
+  });
+</script>
+
+<style scoped>
+  .section--header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    padding-bottom: 8px;
+    border-bottom: 1px solid #e0e0e0;
+  }
+
+  .my--requests--wrap {
+    background: #f8f9fa;
+    border-radius: 8px;
+    padding: 16px;
+    margin-bottom: 20px;
+  }
+
+  .request--cards {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+    gap: 16px;
+  }
+
+  .request--card {
+    background: white;
+    border-radius: 8px;
+    padding: 16px;
+    border-left: 4px solid #e0e0e0;
+  }
+
+  .request--card.status-pending {
+    border-left-color: #ff9800;
+  }
+
+  .request--card.status-approved {
+    border-left-color: #4caf50;
+  }
+
+  .request--card.status-rejected {
+    border-left-color: #f44336;
+  }
+
+  .card--header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 12px;
+  }
+
+  .card--header h4 {
+    margin: 0;
+    font-size: 16px;
+    font-weight: 600;
+  }
+
+  .card--content {
+    margin-bottom: 12px;
+  }
+
+  .card--content p {
+    margin: 4px 0;
+    font-size: 14px;
+    color: #666;
+  }
+
+  .request--message {
+    font-style: italic;
+    color: #333 !important;
+  }
+
+  .card--actions {
+    display: flex;
+    gap: 8px;
+  }
+
+  .vendor--search--wrap {
+    margin-top: 20px;
+  }
+
+  .vendor--grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+    gap: 20px;
+    margin-bottom: 20px;
+  }
+
+  .vendor--card {
+    background: white;
+    border-radius: 12px;
+    padding: 20px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    transition: transform 0.2s, box-shadow 0.2s;
+  }
+
+  .vendor--card:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+  }
+
+  .vendor--card--header {
+    display: flex;
+    gap: 16px;
+    margin-bottom: 16px;
+  }
+
+  .vendor--logo {
+    width: 60px;
+    height: 60px;
+    border-radius: 8px;
+    overflow: hidden;
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: #f5f5f5;
+  }
+
+  .no-logo {
+    font-size: 24px;
+    font-weight: bold;
+    color: #666;
+  }
+
+  .vendor--info h3 {
+    margin: 0 0 4px 0;
+    font-size: 18px;
+    font-weight: 600;
+  }
+
+  .vendor--category {
+    color: #666;
+    font-size: 14px;
+    margin: 0 0 8px 0;
+  }
+
+  .vendor--meta {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+  }
+
+  .vendor--meta span {
+    font-size: 12px;
+    color: #888;
+  }
+
+  .vendor--card--body {
+    margin-bottom: 16px;
+  }
+
+  .vendor--description {
+    font-size: 14px;
+    color: #666;
+    line-height: 1.4;
+    margin-bottom: 12px;
+  }
+
+  .vendor--tags {
+    margin-bottom: 8px;
+  }
+
+  .vendor--card--footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .partnership--status {
+    flex: 1;
+  }
+
+  .status-badge {
+    padding: 4px 8px;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 500;
+  }
+
+  .status-badge.available {
+    background: #e8f5e8;
+    color: #2e7d32;
+  }
+
+  .status-badge.pending {
+    background: #fff3e0;
+    color: #ef6c00;
+  }
+
+  .status-badge.approved {
+    background: #e8f5e8;
+    color: #2e7d32;
+  }
+
+  .status-badge.rejected {
+    background: #ffebee;
+    color: #c62828;
+  }
+
+  .card--actions {
+    display: flex;
+    gap: 8px;
+  }
+
+  .loading-wrap,
+  .error-wrap,
+  .no-data-wrap {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+  }
+
+  .no-data {
+    text-align: center;
+  }
+
+  .no-data h3 {
+    margin: 16px 0 8px;
+    color: #666;
+  }
+
+  .no-data p {
+    color: #999;
+  }
+
+  .pagination-wrap {
+    display: flex;
+    justify-content: center;
+    margin-top: 20px;
+  }
+
+  .request--form {
+    padding: 8px 0;
+  }
+
+  .vendor--summary {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px;
+    background: #f8f9fa;
+    border-radius: 8px;
+    margin-bottom: 16px;
+  }
+
+  .vendor--logo--small {
+    width: 40px;
+    height: 40px;
+    border-radius: 6px;
+    overflow: hidden;
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: #f5f5f5;
+  }
+
+  .no-logo--small {
+    font-size: 16px;
+    font-weight: bold;
+    color: #666;
+  }
+
+  .form--section {
+    margin-top: 16px;
+  }
+
+  .form--section h5 {
+    margin: 0 0 8px 0;
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+  }
+
+  .result-count {
+    font-size: 14px;
+    color: #666;
+    font-weight: 500;
+  }
+</style>

+ 592 - 283
pages/view/vendor/vendors.vue

@@ -4,316 +4,625 @@
       <h2>{{ pageId }}</h2>
       <div class="bread--crumbs--wrap">
         <span>홈</span>
-        <span>{{ pageId }}</span>
+        <span @click="goBack" class="breadcrumb-link">벤더사 관리</span>
+        <span>{{ currentVendor?.name || "벤더사 상세" }}</span>
       </div>
     </div>
 
-    <!-- 검색 및 필터 영역 -->
-    <div class="search--modules type2">
-      <div class="search--inner">
-        <div class="form--cont--filter">
-          <v-select
-            v-model="selectedCategory"
-            :items="categoryOptions"
-            variant="outlined"
-            class="custom-select"
-            label="카테고리"
-            clearable
-          >
-          </v-select>
-        </div>
-        <div class="form--cont--text">
-          <v-text-field
-            v-model="searchName"
-            class="custom-input mini"
-            style="width: 100%"
-            placeholder="벤더사명을 입력하세요"
-            @keyup.enter="handleSearch"
-          ></v-text-field>
-        </div>
-      </div>
-      <v-btn
-        class="custom-btn btn-blue mini sch--btn"
-        @click="handleSearch"
-        :loading="vendorsStore.getLoading"
-      >
-        검색
-      </v-btn>
+    <!-- 로딩 상태 -->
+    <div v-if="isLoading" class="loading-wrap">
+      <v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
+      <p>벤더사 정보를 불러오고 있습니다...</p>
     </div>
 
-    <!-- 벤더사 리스트 -->
-    <div class="data--list--wrap">
-      <div class="btn--actions--wrap">
-        <div class="left--sections">
-          <span class="result-count">
-            총 {{ vendorsStore.getPagination?.totalCount || 0 }}개의 벤더사
-          </span>
-        </div>
-      </div>
-
-      <!-- 로딩 상태 -->
-      <div v-if="vendorsStore.getLoading" class="loading-wrap">
-        <v-progress-circular indeterminate color="primary"></v-progress-circular>
-        <p>벤더사를 검색하고 있습니다...</p>
-      </div>
-
-      <!-- 에러 상태 -->
-      <div v-else-if="vendorsStore.getError" class="error-wrap">
-        <v-alert type="error" dismissible @click:close="vendorsStore.clearError()">
-          {{ vendorsStore.getError }}
-        </v-alert>
-      </div>
+    <!-- 에러 상태 -->
+    <div v-else-if="errorMessage" class="error-wrap">
+      <v-alert type="error" dismissible @click:close="vendorsStore.clearError()">
+        {{ errorMessage }}
+      </v-alert>
+      <v-btn @click="goBack" class="custom-btn btn-blue">목록으로 돌아가기</v-btn>
+    </div>
 
-      <!-- 벤더사 리스트 -->
-      <div v-else-if="vendorsStore.getVendors?.length > 0" class="vendor--list--wrap">
-        <v-data-table
-          :headers="headers"
-          :items="vendorsStore.getVendors"
-          :loading="vendorsStore.getLoading"
-          class="custom-data-table"
-          @click:row="handleRowClick"
-        >
-          <template #item.logo="{ item }">
-            <v-avatar size="40" class="vendor-logo">
-              <v-img
-                v-if="item.logo"
-                :src="item.logo"
-                :alt="item.name + ' 로고'"
-              ></v-img>
-              <div v-else class="no-logo">{{ item.name.charAt(0) }}</div>
-            </v-avatar>
-          </template>
-          
-          <template #item.category="{ item }">
-            <v-chip
-              :color="getCategoryColor(item.category)"
-              size="small"
-              variant="outlined"
-            >
-              {{ item.category }}
-            </v-chip>
-          </template>
-
-          <template #item.status="{ item }">
-            <v-chip
-              :color="item.status === 'ACTIVE' ? 'success' : 'error'"
-              size="small"
-            >
-              {{ item.status === 'ACTIVE' ? '활성' : '비활성' }}
-            </v-chip>
-          </template>
-
-          <template #item.actions="{ item }">
-            <v-btn
-              icon="mdi-eye"
-              size="small"
-              variant="text"
-              @click.stop="viewVendorDetail(item.id)"
-            >
-            </v-btn>
-          </template>
-        </v-data-table>
-
-        <!-- 페이지네이션 -->
-        <div class="pagination-wrap" v-if="(vendorsStore.getPagination?.totalPages || 0) > 1">
-          <v-pagination
-            v-model="currentPage"
-            :length="vendorsStore.getPagination?.totalPages || 1"
-            :total-visible="7"
-            @update:model-value="handlePageChange"
-          ></v-pagination>
-        </div>
-      </div>
+    <!-- 벤더사 상세 정보 -->
+    <div v-else-if="currentVendor" class="vendor-detail-wrap">
+      <!-- 벤더사 기본 정보 -->
+      <v-card class="vendor-header-card" elevation="2">
+        <v-card-text>
+          <div class="vendor-header">
+            <div class="vendor-logo-section">
+              <v-avatar size="80" class="vendor-logo-large">
+                <v-img
+                  v-if="currentVendor.logo"
+                  :src="currentVendor.logo"
+                  :alt="currentVendor.name + ' 로고'"
+                ></v-img>
+                <div v-else class="no-logo-large">{{ currentVendor.name.charAt(0) }}</div>
+              </v-avatar>
+            </div>
+            <div class="vendor-info-section">
+              <h1 class="vendor-name">{{ currentVendor.name }}</h1>
+              <div class="vendor-meta">
+                <v-chip
+                  :color="getCategoryColor(currentVendor.category)"
+                  size="large"
+                  variant="outlined"
+                  class="mr-2"
+                >
+                  {{ getCategoryName(currentVendor.category) }}
+                </v-chip>
+                <v-chip
+                  :color="currentVendor.status === 'ACTIVE' ? 'success' : 'error'"
+                  size="large"
+                >
+                  {{ currentVendor.status === "ACTIVE" ? "활성" : "비활성" }}
+                </v-chip>
+              </div>
+              <p v-if="currentVendor.description" class="vendor-description">
+                {{ currentVendor.description }}
+              </p>
+            </div>
+            <div class="vendor-actions">
+              <v-btn
+                v-if="currentVendor.website"
+                :href="currentVendor.website"
+                target="_blank"
+                class="custom-btn btn-white mr-2"
+                prepend-icon="mdi-web"
+              >
+                웹사이트
+              </v-btn>
+              <v-btn
+                @click="goBack"
+                class="custom-btn btn-blue"
+                prepend-icon="mdi-arrow-left"
+              >
+                목록으로
+              </v-btn>
+            </div>
+          </div>
+        </v-card-text>
+      </v-card>
+
+      <!-- 상세 정보 탭 -->
+      <v-card class="detail-tabs-card" elevation="2">
+        <v-tabs v-model="activeTab" class="custom-tabs">
+          <v-tab value="info">기업 정보</v-tab>
+          <v-tab value="contact">연락처</v-tab>
+          <v-tab value="products">제품 정보</v-tab>
+          <v-tab value="partnership">파트너십</v-tab>
+        </v-tabs>
+
+        <v-card-text>
+          <v-tabs-window v-model="activeTab">
+            <!-- 기업 정보 탭 -->
+            <v-tabs-window-item value="info">
+              <div class="info-section">
+                <v-row>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>사업자등록번호</h3>
+                      <p>{{ currentVendor.businessNumber || "-" }}</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>설립일</h3>
+                      <p>{{ formatDate(currentVendor.establishedDate) || "-" }}</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>직원 수</h3>
+                      <p>
+                        {{
+                          currentVendor.employeeCount
+                            ? currentVendor.employeeCount + "명"
+                            : "-"
+                        }}
+                      </p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>연매출</h3>
+                      <p>{{ formatCurrency(currentVendor.annualRevenue) || "-" }}</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12">
+                    <div class="info-item">
+                      <h3>사업 분야</h3>
+                      <div class="business-areas">
+                        <v-chip
+                          v-for="area in currentVendor.businessAreas || []"
+                          :key="area"
+                          size="small"
+                          variant="outlined"
+                          class="mr-2 mb-2"
+                        >
+                          {{ area }}
+                        </v-chip>
+                      </div>
+                    </div>
+                  </v-col>
+                </v-row>
+              </div>
+            </v-tabs-window-item>
+
+            <!-- 연락처 탭 -->
+            <v-tabs-window-item value="contact">
+              <div class="contact-section">
+                <v-row>
+                  <v-col cols="12" md="6">
+                    <v-card variant="outlined" class="contact-card">
+                      <v-card-title>
+                        <v-icon class="mr-2">mdi-account</v-icon>
+                        주요 담당자
+                      </v-card-title>
+                      <v-card-text>
+                        <div class="contact-item">
+                          <strong>이름:</strong> {{ currentVendor.contactName || "-" }}
+                        </div>
+                        <div class="contact-item">
+                          <strong>직책:</strong>
+                          {{ currentVendor.contactPosition || "-" }}
+                        </div>
+                        <div class="contact-item">
+                          <strong>전화:</strong>
+                          <a
+                            v-if="currentVendor.contactPhone"
+                            :href="`tel:${currentVendor.contactPhone}`"
+                          >
+                            {{ currentVendor.contactPhone }}
+                          </a>
+                          <span v-else>-</span>
+                        </div>
+                        <div class="contact-item">
+                          <strong>이메일:</strong>
+                          <a
+                            v-if="currentVendor.contactEmail"
+                            :href="`mailto:${currentVendor.contactEmail}`"
+                          >
+                            {{ currentVendor.contactEmail }}
+                          </a>
+                          <span v-else>-</span>
+                        </div>
+                      </v-card-text>
+                    </v-card>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <v-card variant="outlined" class="contact-card">
+                      <v-card-title>
+                        <v-icon class="mr-2">mdi-map-marker</v-icon>
+                        주소 정보
+                      </v-card-title>
+                      <v-card-text>
+                        <div class="contact-item">
+                          <strong>주소:</strong>
+                          <p>{{ currentVendor.address || "-" }}</p>
+                        </div>
+                        <div class="contact-item">
+                          <strong>상세주소:</strong>
+                          <p>{{ currentVendor.detailAddress || "-" }}</p>
+                        </div>
+                        <div class="contact-item">
+                          <strong>우편번호:</strong> {{ currentVendor.zipCode || "-" }}
+                        </div>
+                      </v-card-text>
+                    </v-card>
+                  </v-col>
+                </v-row>
+              </div>
+            </v-tabs-window-item>
+
+            <!-- 제품 정보 탭 -->
+            <v-tabs-window-item value="products">
+              <div class="products-section">
+                <div class="section-header">
+                  <h3>주요 제품/서비스</h3>
+                </div>
+                <v-row v-if="currentVendor.products && currentVendor.products.length > 0">
+                  <v-col
+                    v-for="product in currentVendor.products"
+                    :key="product.id"
+                    cols="12"
+                    md="6"
+                    lg="4"
+                  >
+                    <v-card class="product-card" variant="outlined">
+                      <v-img
+                        v-if="product.image"
+                        :src="product.image"
+                        height="150"
+                        cover
+                      ></v-img>
+                      <v-card-title>{{ product.name }}</v-card-title>
+                      <v-card-text>
+                        <p>{{ product.description }}</p>
+                        <div class="product-price" v-if="product.price">
+                          {{ formatCurrency(product.price) }}
+                        </div>
+                      </v-card-text>
+                    </v-card>
+                  </v-col>
+                </v-row>
+                <div v-else class="no-data">
+                  <v-icon size="48" color="grey-lighten-1">mdi-package-variant</v-icon>
+                  <p>등록된 제품 정보가 없습니다</p>
+                </div>
+              </div>
+            </v-tabs-window-item>
+
+            <!-- 파트너십 탭 -->
+            <v-tabs-window-item value="partnership">
+              <div class="partnership-section">
+                <v-row>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>파트너십 등급</h3>
+                      <v-chip
+                        :color="getPartnershipColor(currentVendor.partnershipLevel)"
+                        size="large"
+                      >
+                        {{ getPartnershipName(currentVendor.partnershipLevel) }}
+                      </v-chip>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>협력 시작일</h3>
+                      <p>{{ formatDate(currentVendor.partnershipStartDate) || "-" }}</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>협력 프로젝트 수</h3>
+                      <p>{{ currentVendor.projectCount || 0 }}개</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>평점</h3>
+                      <div class="rating">
+                        <v-rating
+                          v-model="currentVendor.rating"
+                          readonly
+                          size="small"
+                          density="compact"
+                        ></v-rating>
+                        <span class="rating-text">{{ currentVendor.rating || 0 }}/5</span>
+                      </div>
+                    </div>
+                  </v-col>
+                  <v-col cols="12">
+                    <div class="info-item">
+                      <h3>특이사항</h3>
+                      <p>{{ currentVendor.notes || "특이사항이 없습니다." }}</p>
+                    </div>
+                  </v-col>
+                </v-row>
+              </div>
+            </v-tabs-window-item>
+          </v-tabs-window>
+        </v-card-text>
+      </v-card>
+    </div>
 
-      <!-- 검색 결과 없음 -->
-      <div v-else class="no-data-wrap">
-        <div class="no-data">
-          <v-icon size="64" color="grey-lighten-1">mdi-store-search</v-icon>
-          <h3>검색된 벤더사가 없습니다</h3>
-          <p>다른 검색 조건을 시도해보세요</p>
-        </div>
+    <!-- 데이터가 없을 때 -->
+    <div v-else class="no-data-wrap">
+      <div class="no-data">
+        <v-icon size="64" color="grey-lighten-1">mdi-store-alert</v-icon>
+        <h3>벤더사 정보를 찾을 수 없습니다</h3>
+        <p>요청하신 벤더사가 존재하지 않거나 삭제되었을 수 있습니다</p>
+        <v-btn @click="goBack" class="custom-btn btn-blue">목록으로 돌아가기</v-btn>
       </div>
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, onMounted, computed } from 'vue'
-import { useRouter } from 'vue-router'
-import { useVendorsStore } from '@/stores/vendors'
+  import { ref, onMounted, computed } from "vue";
+  import { useRoute, useRouter } from "vue-router";
+  import { useVendorsStore } from "@/stores/vendors";
 
-/************************************************************************
+  /************************************************************************
 |    레이아웃
 ************************************************************************/
-definePageMeta({
-  layout: "default",
-})
+  definePageMeta({
+    layout: "default",
+  });
 
-/************************************************************************
-|    스토어 & 라우터
+  /************************************************************************
+|    스토어, 라우터, 라우트
 ************************************************************************/
-const vendorsStore = useVendorsStore()
-const router = useRouter()
+  const vendorsStore = useVendorsStore();
+  const router = useRouter();
+  const route = useRoute();
 
-/************************************************************************
+  /************************************************************************
 |    반응형 데이터
 ************************************************************************/
-const pageId = ref("벤더사 관리")
-const searchName = ref("")
-const selectedCategory = ref("")
-const currentPage = ref(1)
-
-const categoryOptions = ref([
-  { title: "전체", value: "" },
-  { title: "패션·뷰티", value: "FASHION_BEAUTY" },
-  { title: "식품·건강", value: "FOOD_HEALTH" },
-  { title: "라이프스타일", value: "LIFESTYLE" },
-  { title: "테크·가전", value: "TECH_ELECTRONICS" },
-  { title: "스포츠·레저", value: "SPORTS_LEISURE" },
-  { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" }
-])
-
-const headers = [
-  { title: "로고", key: "logo", sortable: false, width: "80px" },
-  { title: "벤더사명", key: "name", sortable: true },
-  { title: "카테고리", key: "category", sortable: true },
-  { title: "담당자", key: "contactName", sortable: false },
-  { title: "연락처", key: "contactPhone", sortable: false },
-  { title: "이메일", key: "contactEmail", sortable: false },
-  { title: "상태", key: "status", sortable: true },
-  { title: "등록일", key: "createdAt", sortable: true },
-  { title: "액션", key: "actions", sortable: false, width: "100px" }
-]
-
-/************************************************************************
-|    computed
+  const pageId = ref("벤더사 상세");
+  const activeTab = ref("info");
+
+  /************************************************************************
+|    computed - .value 제거!
 ************************************************************************/
-const currentSearchConditions = computed(() => vendorsStore.getSearchConditions)
+  const currentVendor = computed(() => vendorsStore.getCurrentVendor);
+  const isLoading = computed(() => vendorsStore.getLoading);
+  const errorMessage = computed(() => vendorsStore.getError);
 
-/************************************************************************
+  /************************************************************************
 |    메서드
 ************************************************************************/
-const handleSearch = async () => {
-  const conditions = {
-    name: searchName.value,
-    category: selectedCategory.value,
-    page: 1,
-    size: 10
-  }
-  
-  currentPage.value = 1
-  await vendorsStore.searchVendors(conditions)
-}
-
-const handlePageChange = async (page) => {
-  currentPage.value = page
-  const conditions = {
-    ...currentSearchConditions.value,
-    page: page
-  }
-  
-  await vendorsStore.searchVendors(conditions)
-}
-
-const handleRowClick = (event, { item }) => {
-  if (item?.id) {
-    viewVendorDetail(item.id)
-  }
-}
-
-const viewVendorDetail = (vendorId) => {
-  router.push(`/view/vendor/${vendorId}`)
-}
-
-const getCategoryColor = (category) => {
-  const colors = {
-    'FASHION_BEAUTY': 'pink',
-    'FOOD_HEALTH': 'green',
-    'LIFESTYLE': 'blue',
-    'TECH_ELECTRONICS': 'purple',
-    'SPORTS_LEISURE': 'orange',
-    'CULTURE_ENTERTAINMENT': 'red'
-  }
-  return colors[category] || 'grey'
-}
-
-/************************************************************************
+  const goBack = () => {
+    router.push("/view/vendor/vendors");
+  };
+
+  const getCategoryColor = (category) => {
+    const colors = {
+      FASHION_BEAUTY: "pink",
+      FOOD_HEALTH: "green",
+      LIFESTYLE: "blue",
+      TECH_ELECTRONICS: "purple",
+      SPORTS_LEISURE: "orange",
+      CULTURE_ENTERTAINMENT: "red",
+    };
+    return colors[category] || "grey";
+  };
+
+  const getCategoryName = (category) => {
+    const names = {
+      FASHION_BEAUTY: "패션·뷰티",
+      FOOD_HEALTH: "식품·건강",
+      LIFESTYLE: "라이프스타일",
+      TECH_ELECTRONICS: "테크·가전",
+      SPORTS_LEISURE: "스포츠·레저",
+      CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
+    };
+    return names[category] || category;
+  };
+
+  const getPartnershipColor = (level) => {
+    const colors = {
+      PLATINUM: "purple",
+      GOLD: "amber",
+      SILVER: "grey",
+      BRONZE: "brown",
+      BASIC: "blue-grey",
+    };
+    return colors[level] || "grey";
+  };
+
+  const getPartnershipName = (level) => {
+    const names = {
+      PLATINUM: "플래티넘",
+      GOLD: "골드",
+      SILVER: "실버",
+      BRONZE: "브론즈",
+      BASIC: "베이직",
+    };
+    return names[level] || level;
+  };
+
+  const formatDate = (dateString) => {
+    if (!dateString) return null;
+    return new Date(dateString).toLocaleDateString("ko-KR");
+  };
+
+  const formatCurrency = (amount) => {
+    if (!amount) return null;
+    return new Intl.NumberFormat("ko-KR", {
+      style: "currency",
+      currency: "KRW",
+    }).format(amount);
+  };
+
+  /************************************************************************
 |    라이프사이클
 ************************************************************************/
-onMounted(async () => {
-  // 초기 검색 실행 (전체 벤더사 로드)
-  await vendorsStore.searchVendors({
-    name: '',
-    category: '',
-    page: 1,
-    size: 10
-  })
-})
+  onMounted(async () => {
+    const vendorId = route.params.id;
+    if (vendorId) {
+      try {
+        await vendorsStore.getVendorById(vendorId);
+      } catch (error) {
+        console.error("벤더사 정보 로드 실패:", error);
+      }
+    }
+  });
 </script>
 
 <style scoped>
-.vendor--list--wrap {
-  margin-top: 20px;
-}
-
-.loading-wrap, .error-wrap, .no-data-wrap {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  padding: 60px 20px;
-}
-
-.no-data {
-  text-align: center;
-}
-
-.no-data h3 {
-  margin: 16px 0 8px;
-  color: #666;
-}
-
-.no-data p {
-  color: #999;
-}
-
-.vendor-logo {
-  border: 1px solid #e0e0e0;
-}
-
-.no-logo {
-  background: #f5f5f5;
-  color: #666;
-  font-weight: bold;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-}
-
-.result-count {
-  font-size: 14px;
-  color: #666;
-  font-weight: 500;
-}
-
-.pagination-wrap {
-  display: flex;
-  justify-content: center;
-  margin-top: 20px;
-}
-
-.custom-data-table {
-  background: white;
-  border-radius: 8px;
-}
-
-.custom-data-table :deep(.v-data-table__tr) {
-  cursor: pointer;
-}
-
-.custom-data-table :deep(.v-data-table__tr:hover) {
-  background-color: #f5f5f5;
-}
-</style>
+  .vendor-detail-wrap {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+  }
+
+  .vendor-header-card {
+    margin-bottom: 20px;
+  }
+
+  .vendor-header {
+    display: flex;
+    align-items: flex-start;
+    gap: 20px;
+  }
+
+  .vendor-logo-section {
+    flex-shrink: 0;
+  }
+
+  .vendor-logo-large {
+    border: 1px solid #e0e0e0;
+  }
+
+  .no-logo-large {
+    background: #f5f5f5;
+    color: #666;
+    font-weight: bold;
+    font-size: 32px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+  }
+
+  .vendor-info-section {
+    flex: 1;
+  }
+
+  .vendor-name {
+    font-size: 28px;
+    font-weight: bold;
+    margin-bottom: 12px;
+  }
+
+  .vendor-meta {
+    margin-bottom: 16px;
+  }
+
+  .vendor-description {
+    color: #666;
+    line-height: 1.6;
+    margin: 0;
+  }
+
+  .vendor-actions {
+    flex-shrink: 0;
+  }
+
+  .breadcrumb-link {
+    cursor: pointer;
+    color: #1976d2;
+  }
+
+  .breadcrumb-link:hover {
+    text-decoration: underline;
+  }
+
+  .detail-tabs-card {
+    margin-top: 20px;
+  }
+
+  .info-section,
+  .contact-section,
+  .products-section,
+  .partnership-section {
+    padding: 20px 0;
+  }
+
+  .info-item {
+    margin-bottom: 24px;
+  }
+
+  .info-item h3 {
+    font-size: 16px;
+    font-weight: 600;
+    margin-bottom: 8px;
+    color: #333;
+  }
+
+  .info-item p {
+    font-size: 14px;
+    color: #666;
+    margin: 0;
+  }
+
+  .contact-card {
+    height: 100%;
+  }
+
+  .contact-item {
+    margin-bottom: 12px;
+  }
+
+  .contact-item strong {
+    display: inline-block;
+    width: 80px;
+    color: #333;
+  }
+
+  .contact-item a {
+    color: #1976d2;
+    text-decoration: none;
+  }
+
+  .contact-item a:hover {
+    text-decoration: underline;
+  }
+
+  .section-header {
+    margin-bottom: 20px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #e0e0e0;
+  }
+
+  .section-header h3 {
+    font-size: 18px;
+    font-weight: 600;
+    margin: 0;
+  }
+
+  .product-card {
+    height: 100%;
+  }
+
+  .product-price {
+    font-weight: bold;
+    color: #1976d2;
+    margin-top: 8px;
+  }
+
+  .rating {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .rating-text {
+    font-size: 14px;
+    color: #666;
+  }
+
+  .business-areas {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
+  .loading-wrap,
+  .error-wrap,
+  .no-data-wrap {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+  }
+
+  .no-data {
+    text-align: center;
+  }
+
+  .no-data h3 {
+    margin: 16px 0 8px;
+    color: #666;
+  }
+
+  .no-data p {
+    color: #999;
+    margin-bottom: 20px;
+  }
+
+  @media (max-width: 768px) {
+    .vendor-header {
+      flex-direction: column;
+    }
+
+    .vendor-actions {
+      width: 100%;
+    }
+  }
+</style>

+ 0 - 3
server/tsconfig.json

@@ -1,3 +0,0 @@
-{
-  "extends": "../.nuxt/tsconfig.server.json"
-}

+ 32 - 9
stores/auth.js

@@ -12,7 +12,7 @@ export const useAuthStore = defineStore('authStore', () => {
     snsTempData : '', // sns 임시데이터
   })
 
-                              // 전체 조회
+  // 전체 조회
   const getCompanyId = computed(() => auth.value.companyId)             // 아이디 조회
   const getSeq = computed(() => auth.value.seq)             // 시퀀스 조회
   const getUserId = computed(() => auth.value.id)             // 아이디 조회
@@ -23,7 +23,6 @@ export const useAuthStore = defineStore('authStore', () => {
   const getAccessToken = computed(() => auth.value.accessToken)         // 토큰 조회
   const getRefreshToken = computed(() => auth.value.refreshToken)       // 리프레시토큰 조회
   const getSnsTempData = computed(() => auth.value.snsTempData)       // sns 임시데이터 조회
-       // 점검공지
 
   function setAuth(payload){
     auth.value.companyId = payload.user.COMP_ID
@@ -49,16 +48,40 @@ export const useAuthStore = defineStore('authStore', () => {
     auth.value.refreshToken = token
   }
 
-
   // logout
   function setLogout(){
-    auth.value.companyId = ''
-    auth.value.accessToken = ''
-    auth.value.refreshToken = ''    
-    // Pinia persist가 사용하는 localStorage 키 삭제
-    localStorage.removeItem('authStore');
+    // 모든 필드 초기화
+    auth.value = {
+      seq: '',
+      id: '',
+      name: '',
+      email: '',
+      companyName: '',
+      phone: '',
+      companyId: '',
+      accessToken: '',
+      refreshToken: '',
+      snsTempData: ''
+    }
   }
 
-  return { auth, getCompanyId, getSnsTempData, getAccessToken, getRefreshToken, setAuth, setTempData, setAccessToken, setRefreshToken, setLogout, getSeq, getUserId, getUserName, getUserEmail, getCompanyName, getUserPhone  }
+  return { 
+    auth, 
+    getCompanyId, 
+    getSnsTempData, 
+    getAccessToken, 
+    getRefreshToken, 
+    setAuth, 
+    setTempData, 
+    setAccessToken, 
+    setRefreshToken, 
+    setLogout,
+    getSeq,
+    getUserId,
+    getUserName,
+    getUserEmail,
+    getCompanyName,
+    getUserPhone
+  }
 }, {persist: { storage: persistedState.localStorage}})