influencer-requests.vue 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369
  1. <template>
  2. <div>
  3. <div class="inner--headers">
  4. <h2>{{ pageId }}</h2>
  5. <div class="bread--crumbs--wrap">
  6. <span>홈</span>
  7. <span>벤더 대시보드</span>
  8. <span>{{ pageId }}</span>
  9. </div>
  10. </div>
  11. <!-- 통계 카드 -->
  12. <div class="stats--cards--wrap">
  13. <div class="stats--card">
  14. <div class="stats--icon pending">
  15. <v-icon>mdi-clock-outline</v-icon>
  16. </div>
  17. <div class="stats--content">
  18. <h3>{{ stats.pending || 0 }}</h3>
  19. <p>대기 중인 승인요청</p>
  20. </div>
  21. </div>
  22. <div class="stats--card">
  23. <div class="stats--icon approved">
  24. <v-icon>mdi-check-circle</v-icon>
  25. </div>
  26. <div class="stats--content">
  27. <h3>{{ stats.approved || 0 }}</h3>
  28. <p>승인 완료</p>
  29. </div>
  30. </div>
  31. <div class="stats--card">
  32. <div class="stats--icon rejected">
  33. <v-icon>mdi-close-circle</v-icon>
  34. </div>
  35. <div class="stats--content">
  36. <h3>{{ stats.rejected || 0 }}</h3>
  37. <p>거부</p>
  38. </div>
  39. </div>
  40. <div class="stats--card">
  41. <div class="stats--icon total">
  42. <v-icon>mdi-account-group</v-icon>
  43. </div>
  44. <div class="stats--content">
  45. <h3>{{ stats.total || 0 }}</h3>
  46. <p>총 요청 수</p>
  47. </div>
  48. </div>
  49. </div>
  50. <!-- 필터 및 검색 -->
  51. <div class="search--modules type2">
  52. <div class="search--inner">
  53. <div class="form--cont--filter">
  54. <v-select
  55. v-model="searchFilter.status"
  56. :items="statusOptions"
  57. variant="outlined"
  58. class="custom-select"
  59. label="상태"
  60. clearable
  61. >
  62. </v-select>
  63. </div>
  64. <div class="form--cont--filter">
  65. <v-select
  66. v-model="searchFilter.category"
  67. :items="categoryOptions"
  68. variant="outlined"
  69. class="custom-select"
  70. label="인플루언서 카테고리"
  71. clearable
  72. >
  73. </v-select>
  74. </div>
  75. <div class="form--cont--text">
  76. <v-text-field
  77. v-model="searchFilter.keyword"
  78. class="custom-input mini"
  79. style="width: 100%"
  80. placeholder="인플루언서명을 입력하세요"
  81. @keyup.enter="handleSearch"
  82. ></v-text-field>
  83. </div>
  84. </div>
  85. <v-btn
  86. class="custom-btn btn-blue mini sch--btn"
  87. @click="handleSearch"
  88. :loading="loading"
  89. >
  90. 검색
  91. </v-btn>
  92. </div>
  93. <!-- 인플루언서 승인 요청 목록 -->
  94. <div class="data--list--wrap">
  95. <div class="btn--actions--wrap">
  96. <div class="left--sections">
  97. <span class="result-count">
  98. 총 {{ pagination.totalCount || 0 }}개의 승인요청
  99. </span>
  100. </div>
  101. <div class="right--sections">
  102. <v-select
  103. v-model="sortOption"
  104. :items="sortOptions"
  105. variant="outlined"
  106. class="custom-select mini"
  107. @update:model-value="handleSort"
  108. >
  109. </v-select>
  110. </div>
  111. </div>
  112. <!-- 로딩 상태 -->
  113. <div v-if="loading" class="loading-wrap">
  114. <v-progress-circular indeterminate color="primary"></v-progress-circular>
  115. <p>승인요청을 불러오고 있습니다...</p>
  116. </div>
  117. <!-- 에러 상태 -->
  118. <div v-else-if="error" class="error-wrap">
  119. <v-alert type="error" dismissible @click:close="error = null">
  120. {{ error }}
  121. </v-alert>
  122. </div>
  123. <!-- 승인요청 리스트 -->
  124. <div v-else-if="requests.length > 0" class="requests--list--wrap">
  125. <div class="requests--grid">
  126. <div
  127. v-for="request in requests"
  128. :key="request.SEQ"
  129. class="request--card"
  130. :class="getRequestStatusClass(request.STATUS)"
  131. >
  132. <!-- 카드 헤더 -->
  133. <div class="request--card--header">
  134. <div class="influencer--info">
  135. <div class="influencer--avatar">
  136. <v-img
  137. v-if="request.influencerAvatar"
  138. :src="request.influencerAvatar"
  139. :alt="request.influencerNickname + ' 프로필'"
  140. width="50"
  141. height="50"
  142. ></v-img>
  143. <div v-else class="no-avatar">
  144. {{ request.influencerNickname?.charAt(0) || "U" }}
  145. </div>
  146. </div>
  147. <div class="influencer--details">
  148. <div class="influencer--header">
  149. <h4>{{ request.influencerNickname || request.influencerName }}</h4>
  150. <p class="influencer--category">
  151. {{ getCategoryText(request.influencerCategory) }}
  152. </p>
  153. </div>
  154. <div class="influencer--contact">
  155. <p v-if="request.influencerEmail" class="contact--item">
  156. <v-icon size="small">mdi-email</v-icon>
  157. {{ request.influencerEmail }}
  158. </p>
  159. <p v-if="request.influencerPhone" class="contact--item">
  160. <v-icon size="small">mdi-phone</v-icon>
  161. {{ request.influencerPhone }}
  162. </p>
  163. <p v-if="request.influencerRegion" class="contact--item">
  164. <v-icon size="small">mdi-map-marker</v-icon>
  165. {{ request.influencerRegion }}
  166. </p>
  167. </div>
  168. <div class="influencer--meta">
  169. <span v-if="request.followerCount" class="meta--item">
  170. <v-icon size="small">mdi-account-group</v-icon>
  171. {{ formatNumber(request.followerCount) }} 팔로워
  172. </span>
  173. <span v-if="request.avgViews" class="meta--item">
  174. <v-icon size="small">mdi-eye</v-icon>
  175. 평균 {{ formatNumber(request.avgViews) }} 조회
  176. </span>
  177. <span v-if="request.engagementRate" class="meta--item">
  178. <v-icon size="small">mdi-chart-line</v-icon>
  179. 참여율 {{ request.engagementRate }}%
  180. </span>
  181. </div>
  182. <div v-if="request.influencerDescription" class="influencer--description">
  183. <p>{{ request.influencerDescription }}</p>
  184. </div>
  185. <div v-if="request.influencerSnsChannels" class="influencer--sns">
  186. <div v-for="(channel, index) in parseSnsChannels(request.influencerSnsChannels)"
  187. :key="index"
  188. class="sns--item">
  189. <v-icon size="small">{{ getSnsIcon(channel.platform) }}</v-icon>
  190. {{ channel.handle }}
  191. </div>
  192. </div>
  193. </div>
  194. </div>
  195. <div class="request--status">
  196. <v-chip :color="getStatusColor(request.STATUS)" size="small">
  197. {{ getStatusText(request.STATUS) }}
  198. </v-chip>
  199. <p class="request--date">{{ formatDate(request.REQUEST_DATE) }}</p>
  200. </div>
  201. </div>
  202. <!-- 카드 바디 -->
  203. <div class="request--card--body">
  204. <div v-if="request.REQUEST_MESSAGE" class="request--message">
  205. <h5>요청 메시지</h5>
  206. <p>"{{ request.REQUEST_MESSAGE }}"</p>
  207. </div>
  208. <div
  209. v-if="request.COMMISSION_RATE || request.SPECIAL_CONDITIONS"
  210. class="request--conditions"
  211. >
  212. <h5>희망 조건</h5>
  213. <div v-if="request.COMMISSION_RATE" class="condition--item">
  214. <span class="condition--label">희망 수수료율:</span>
  215. <span class="condition--value">{{ request.COMMISSION_RATE }}%</span>
  216. </div>
  217. <div v-if="request.SPECIAL_CONDITIONS" class="condition--item">
  218. <span class="condition--label">특별 조건:</span>
  219. <span class="condition--value">{{ request.SPECIAL_CONDITIONS }}</span>
  220. </div>
  221. </div>
  222. <div v-if="request.STATUS === 'PENDING'" class="expire--info">
  223. <v-icon size="16" color="warning">mdi-clock-alert</v-icon>
  224. <span>만료일: {{ formatDate(request.EXPIRED_DATE) }}</span>
  225. </div>
  226. </div>
  227. <!-- 카드 푸터 (액션 버튼) -->
  228. <div class="request--card--footer">
  229. <div class="card--actions">
  230. <v-btn
  231. class="custom-btn mini btn-outline"
  232. @click="viewInfluencerDetail(request.INFLUENCER_SEQ)"
  233. >
  234. 프로필 보기
  235. </v-btn>
  236. <div v-if="request.STATUS === 'PENDING'" class="approval--actions">
  237. <v-btn
  238. class="custom-btn mini btn-red"
  239. @click="handleReject(request)"
  240. :loading="processing"
  241. >
  242. 거부
  243. </v-btn>
  244. <v-btn
  245. class="custom-btn mini btn-blue"
  246. @click="handleApprove(request)"
  247. :loading="processing"
  248. >
  249. 승인
  250. </v-btn>
  251. </div>
  252. <div v-else-if="request.STATUS === 'APPROVED'" class="approved--actions">
  253. <v-btn
  254. class="custom-btn mini btn-outline"
  255. @click="viewRequestHistory(request.SEQ)"
  256. >
  257. 이력보기
  258. </v-btn>
  259. <v-btn
  260. class="custom-btn mini btn-terminate"
  261. @click="handleTerminate(request)"
  262. :loading="processing"
  263. >
  264. <v-icon left size="small">mdi-link-off</v-icon>
  265. 해지
  266. </v-btn>
  267. </div>
  268. <v-btn
  269. v-else
  270. class="custom-btn mini btn-outline"
  271. @click="viewRequestHistory(request.SEQ)"
  272. >
  273. 이력보기
  274. </v-btn>
  275. </div>
  276. </div>
  277. </div>
  278. </div>
  279. <!-- 페이지네이션 -->
  280. <div class="pagination-wrap" v-if="pagination.totalPages > 1">
  281. <v-pagination
  282. v-model="currentPage"
  283. :length="pagination.totalPages"
  284. :total-visible="7"
  285. @update:model-value="handlePageChange"
  286. ></v-pagination>
  287. </div>
  288. </div>
  289. <!-- 검색 결과 없음 -->
  290. <div v-else class="no-data-wrap">
  291. <div class="no-data">
  292. <v-icon size="64" color="grey-lighten-1">mdi-account-search</v-icon>
  293. <h3>승인요청이 없습니다</h3>
  294. <p>아직 인플루언서로부터 승인요청이 없습니다</p>
  295. </div>
  296. </div>
  297. </div>
  298. <!-- 승인 확인 모달 -->
  299. <v-dialog v-model="approveModal.show" max-width="500px">
  300. <v-card>
  301. <v-card-title class="text-h5 text-success">
  302. <v-icon left>mdi-check-circle</v-icon>
  303. 승인 확인
  304. </v-card-title>
  305. <v-card-text>
  306. <div class="approve--content">
  307. <div class="influencer--summary">
  308. <div class="influencer--avatar--small">
  309. <v-img
  310. v-if="approveModal.request?.influencerAvatar"
  311. :src="approveModal.request.influencerAvatar"
  312. width="40"
  313. height="40"
  314. ></v-img>
  315. <div v-else class="no-avatar--small">
  316. {{ approveModal.request?.influencerNickname?.charAt(0) || "U" }}
  317. </div>
  318. </div>
  319. <div>
  320. <h4>{{ approveModal.request?.influencerNickname }}</h4>
  321. <p>{{ getCategoryText(approveModal.request?.influencerCategory) }}</p>
  322. </div>
  323. </div>
  324. <p>이 인플루언서의 승인요청을 승인하시겠습니까?</p>
  325. <v-textarea
  326. v-model="approveModal.approveMessage"
  327. label="승인 메시지 (선택사항)"
  328. placeholder="인플루언서에게 전달할 메시지를 입력해주세요..."
  329. rows="3"
  330. counter="300"
  331. maxlength="300"
  332. class="mt-4"
  333. ></v-textarea>
  334. </div>
  335. </v-card-text>
  336. <v-card-actions>
  337. <v-spacer></v-spacer>
  338. <v-btn color="grey" variant="text" @click="closeApproveModal">취소</v-btn>
  339. <v-btn color="success" @click="confirmApprove" :loading="processing">
  340. 승인하기
  341. </v-btn>
  342. </v-card-actions>
  343. </v-card>
  344. </v-dialog>
  345. <!-- 거부 확인 모달 -->
  346. <v-dialog v-model="rejectModal.show" max-width="500px">
  347. <v-card>
  348. <v-card-title class="text-h5 text-error">
  349. <v-icon left>mdi-close-circle</v-icon>
  350. 거부 확인
  351. </v-card-title>
  352. <v-card-text>
  353. <div class="reject--content">
  354. <div class="influencer--summary">
  355. <div class="influencer--avatar--small">
  356. <v-img
  357. v-if="rejectModal.request?.influencerAvatar"
  358. :src="rejectModal.request.influencerAvatar"
  359. width="40"
  360. height="40"
  361. ></v-img>
  362. <div v-else class="no-avatar--small">
  363. {{ rejectModal.request?.influencerNickname?.charAt(0) || "U" }}
  364. </div>
  365. </div>
  366. <div>
  367. <h4>{{ rejectModal.request?.influencerNickname }}</h4>
  368. <p>{{ getCategoryText(rejectModal.request?.influencerCategory) }}</p>
  369. </div>
  370. </div>
  371. <p>이 인플루언서의 승인요청을 거부하시겠습니까?</p>
  372. <v-textarea
  373. v-model="rejectModal.rejectReason"
  374. label="거부 사유"
  375. placeholder="거부 사유를 입력해주세요..."
  376. rows="4"
  377. counter="500"
  378. maxlength="500"
  379. class="mt-4"
  380. required
  381. ></v-textarea>
  382. </div>
  383. </v-card-text>
  384. <v-card-actions>
  385. <v-spacer></v-spacer>
  386. <v-btn color="grey" variant="text" @click="closeRejectModal">취소</v-btn>
  387. <v-btn color="error" @click="confirmReject" :loading="processing">
  388. 거부하기
  389. </v-btn>
  390. </v-card-actions>
  391. </v-card>
  392. </v-dialog>
  393. <!-- 해지 확인 모달 -->
  394. <v-dialog v-model="terminateModal.show" max-width="500px">
  395. <v-card>
  396. <v-card-title class="text-h5 text-warning">
  397. <v-icon left>mdi-link-off</v-icon>
  398. 파트너십 해지 확인
  399. </v-card-title>
  400. <v-card-text>
  401. <div class="terminate--content">
  402. <div class="influencer--summary">
  403. <div class="influencer--avatar--small">
  404. <v-img
  405. v-if="terminateModal.request?.influencerAvatar"
  406. :src="terminateModal.request.influencerAvatar"
  407. width="40"
  408. height="40"
  409. ></v-img>
  410. <div v-else class="no-avatar--small">
  411. {{ terminateModal.request?.influencerNickname?.charAt(0) || "U" }}
  412. </div>
  413. </div>
  414. <div>
  415. <h4>{{ terminateModal.request?.influencerNickname }}</h4>
  416. <p>{{ getCategoryText(terminateModal.request?.influencerCategory) }}</p>
  417. </div>
  418. </div>
  419. <v-alert type="warning" class="mb-4">
  420. <strong>주의:</strong> 파트너십을 해지하면 협업 관계가 종료되며, 이 작업은 되돌릴 수 없습니다.
  421. </v-alert>
  422. <p>이 인플루언서와의 파트너십을 해지하시겠습니까?</p>
  423. <v-textarea
  424. v-model="terminateModal.terminateReason"
  425. label="해지 사유"
  426. placeholder="해지 사유를 입력해주세요..."
  427. rows="4"
  428. counter="500"
  429. maxlength="500"
  430. class="mt-4"
  431. required
  432. ></v-textarea>
  433. </div>
  434. </v-card-text>
  435. <v-card-actions>
  436. <v-spacer></v-spacer>
  437. <v-btn color="grey" variant="text" @click="closeTerminateModal">취소</v-btn>
  438. <v-btn
  439. class="btn-terminate-confirm"
  440. @click="confirmTerminate"
  441. :loading="processing"
  442. >
  443. <v-icon left>mdi-link-off</v-icon>
  444. 해지하기
  445. </v-btn>
  446. </v-card-actions>
  447. </v-card>
  448. </v-dialog>
  449. </div>
  450. </template>
  451. <script setup>
  452. import { ref, onMounted, computed } from "vue";
  453. import { useRouter } from "vue-router";
  454. /************************************************************************
  455. | 레이아웃
  456. ************************************************************************/
  457. definePageMeta({
  458. layout: "default",
  459. });
  460. /************************************************************************
  461. | 스토어 & 라우터
  462. ************************************************************************/
  463. const router = useRouter();
  464. const { $toast } = useNuxtApp();
  465. /************************************************************************
  466. | 반응형 데이터
  467. ************************************************************************/
  468. const pageId = ref("인플루언서 승인요청 관리");
  469. const loading = ref(false);
  470. const processing = ref(false);
  471. const error = ref(null);
  472. const currentPage = ref(1);
  473. // 검색 필터
  474. const searchFilter = ref({
  475. keyword: "",
  476. status: "",
  477. category: "",
  478. });
  479. // 정렬 옵션
  480. const sortOption = ref("latest");
  481. const sortOptions = ref([
  482. { title: "최신순", value: "latest" },
  483. { title: "오래된순", value: "oldest" },
  484. { title: "마감임박순", value: "expiring" },
  485. ]);
  486. // 상태 옵션
  487. const statusOptions = ref([
  488. { title: "전체", value: "" },
  489. { title: "대기중", value: "PENDING" },
  490. { title: "승인완료", value: "APPROVED" },
  491. { title: "거부됨", value: "REJECTED" },
  492. { title: "해지됨", value: "TERMINATED" },
  493. ]);
  494. // 카테고리 옵션
  495. const categoryOptions = ref([
  496. { title: "전체", value: "" },
  497. { title: "패션·뷰티", value: "FASHION_BEAUTY" },
  498. { title: "식품·건강", value: "FOOD_HEALTH" },
  499. { title: "라이프스타일", value: "LIFESTYLE" },
  500. { title: "테크·가전", value: "TECH_ELECTRONICS" },
  501. { title: "스포츠·레저", value: "SPORTS_LEISURE" },
  502. { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" },
  503. ]);
  504. // 데이터
  505. const requests = ref([]);
  506. const stats = ref({
  507. pending: 0,
  508. approved: 0,
  509. rejected: 0,
  510. total: 0,
  511. });
  512. const pagination = ref({
  513. currentPage: 1,
  514. totalPages: 1,
  515. totalCount: 0,
  516. pageSize: 12,
  517. });
  518. // 승인 모달
  519. const approveModal = ref({
  520. show: false,
  521. request: null,
  522. approveMessage: "",
  523. });
  524. // 거부 모달
  525. const rejectModal = ref({
  526. show: false,
  527. request: null,
  528. rejectReason: "",
  529. });
  530. // 해지 모달
  531. const terminateModal = ref({
  532. show: false,
  533. request: null,
  534. terminateReason: "",
  535. });
  536. /************************************************************************
  537. | computed
  538. ************************************************************************/
  539. const currentUser = computed(() => {
  540. const authData = JSON.parse(localStorage.getItem("authStore"))?.auth || {};
  541. console.log('🔍 currentUser (벤더 대시보드):', authData);
  542. return authData;
  543. });
  544. /************************************************************************
  545. | 메서드
  546. ************************************************************************/
  547. const handleSearch = async () => {
  548. currentPage.value = 1;
  549. await loadRequests();
  550. };
  551. const handlePageChange = async (page) => {
  552. currentPage.value = page;
  553. await loadRequests();
  554. };
  555. const handleSort = async () => {
  556. currentPage.value = 1;
  557. await loadRequests();
  558. };
  559. const loadRequests = async () => {
  560. try {
  561. loading.value = true;
  562. error.value = null;
  563. const params = {
  564. vendorSeq: currentUser.value.seq,
  565. keyword: searchFilter.value.keyword,
  566. status: searchFilter.value.status,
  567. category: searchFilter.value.category,
  568. sortBy: sortOption.value,
  569. page: currentPage.value,
  570. size: pagination.value.pageSize,
  571. };
  572. console.log('🔍 loadRequests 호출됨:', params);
  573. useAxios()
  574. .post("/api/vendor-influencer/requests", params)
  575. .then((res) => {
  576. console.log('📥 API 응답:', res.data);
  577. if (res.data.success) {
  578. const items = res.data.data.items;
  579. console.log('📋 받아온 요청 목록:', items.length, items);
  580. // SEQ 중복 확인
  581. const seqs = items.map(item => item.SEQ);
  582. const uniqueSeqs = [...new Set(seqs)];
  583. if (seqs.length !== uniqueSeqs.length) {
  584. console.warn('⚠️ 중복된 SEQ 발견:', seqs);
  585. }
  586. requests.value = items;
  587. pagination.value = res.data.data.pagination;
  588. stats.value = res.data.data.stats || stats.value;
  589. } else {
  590. error.value =
  591. res.data.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
  592. }
  593. })
  594. .catch((err) => {
  595. error.value = err.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
  596. })
  597. .finally(() => {
  598. loading.value = false;
  599. });
  600. } catch (err) {
  601. error.value = err.message || "승인요청 목록을 불러오는 중 오류가 발생했습니다.";
  602. loading.value = false;
  603. }
  604. };
  605. const handleApprove = (request) => {
  606. approveModal.value = {
  607. show: true,
  608. request: request,
  609. approveMessage: "",
  610. };
  611. };
  612. const closeApproveModal = () => {
  613. approveModal.value = {
  614. show: false,
  615. request: null,
  616. approveMessage: "",
  617. };
  618. };
  619. const confirmApprove = async () => {
  620. try {
  621. processing.value = true;
  622. const params = {
  623. mappingSeq: approveModal.value.request.SEQ,
  624. action: "APPROVE",
  625. processedBy: currentUser.value.seq,
  626. responseMessage: approveModal.value.approveMessage,
  627. };
  628. console.log('✅ 승인 처리 시작:', params);
  629. useAxios()
  630. .post("/api/vendor-influencer/approve", params)
  631. .then((res) => {
  632. console.log('📥 승인 처리 응답:', res.data);
  633. if (res.data.success) {
  634. $toast.success("승인요청이 승인되었습니다.");
  635. closeApproveModal();
  636. console.log('🔄 승인 후 목록 새로고침');
  637. loadRequests();
  638. } else {
  639. console.error('❌ 승인 처리 실패:', res.data);
  640. $toast.error(res.data.message || "승인 처리 중 오류가 발생했습니다.");
  641. }
  642. })
  643. .catch((err) => {
  644. $toast.error(err.message || "승인 처리 중 오류가 발생했습니다.");
  645. })
  646. .finally(() => {
  647. processing.value = false;
  648. });
  649. } catch (err) {
  650. $toast.error(err.message || "승인 처리 중 오류가 발생했습니다.");
  651. processing.value = false;
  652. }
  653. };
  654. const handleReject = (request) => {
  655. rejectModal.value = {
  656. show: true,
  657. request: request,
  658. rejectReason: "",
  659. };
  660. };
  661. const closeRejectModal = () => {
  662. rejectModal.value = {
  663. show: false,
  664. request: null,
  665. rejectReason: "",
  666. };
  667. };
  668. const confirmReject = async () => {
  669. if (!rejectModal.value.rejectReason.trim()) {
  670. $toast.error("거부 사유를 입력해주세요.");
  671. return;
  672. }
  673. try {
  674. processing.value = true;
  675. const params = {
  676. mappingSeq: rejectModal.value.request.SEQ,
  677. action: "REJECT",
  678. processedBy: currentUser.value.seq,
  679. responseMessage: rejectModal.value.rejectReason,
  680. };
  681. useAxios()
  682. .post("/api/vendor-influencer/approve", params)
  683. .then((res) => {
  684. if (res.data.success) {
  685. $toast.success("승인요청이 거부되었습니다.");
  686. closeRejectModal();
  687. loadRequests();
  688. } else {
  689. $toast.error(res.data.message || "거부 처리 중 오류가 발생했습니다.");
  690. }
  691. })
  692. .catch((err) => {
  693. $toast.error(err.message || "거부 처리 중 오류가 발생했습니다.");
  694. })
  695. .finally(() => {
  696. processing.value = false;
  697. });
  698. } catch (err) {
  699. $toast.error(err.message || "거부 처리 중 오류가 발생했습니다.");
  700. processing.value = false;
  701. }
  702. };
  703. const viewInfluencerDetail = (influencerSeq) => {
  704. router.push(`/view/influencer/${influencerSeq}`);
  705. };
  706. const viewRequestHistory = (requestSeq) => {
  707. router.push(`/view/vendor/request-history/${requestSeq}`);
  708. };
  709. const handleTerminate = (request) => {
  710. terminateModal.value = {
  711. show: true,
  712. request: request,
  713. terminateReason: "",
  714. };
  715. };
  716. const closeTerminateModal = () => {
  717. terminateModal.value = {
  718. show: false,
  719. request: null,
  720. terminateReason: "",
  721. };
  722. };
  723. const confirmTerminate = async () => {
  724. if (!terminateModal.value.terminateReason.trim()) {
  725. $toast.error("해지 사유를 입력해주세요.");
  726. return;
  727. }
  728. try {
  729. processing.value = true;
  730. const params = {
  731. mappingSeq: terminateModal.value.request.SEQ,
  732. terminateReason: terminateModal.value.terminateReason,
  733. terminatedBy: currentUser.value.seq,
  734. };
  735. console.log('🔗 파트너십 해지 처리 시작:', params);
  736. useAxios()
  737. .post("/api/vendor-influencer/terminate", params)
  738. .then((res) => {
  739. console.log('📥 해지 처리 응답:', res.data);
  740. if (res.data.success) {
  741. $toast.success("파트너십이 해지되었습니다.");
  742. closeTerminateModal();
  743. console.log('🔄 해지 후 목록 새로고침');
  744. loadRequests();
  745. } else {
  746. console.error('❌ 해지 처리 실패:', res.data);
  747. $toast.error(res.data.message || "해지 처리 중 오류가 발생했습니다.");
  748. }
  749. })
  750. .catch((err) => {
  751. $toast.error(err.message || "해지 처리 중 오류가 발생했습니다.");
  752. })
  753. .finally(() => {
  754. processing.value = false;
  755. });
  756. } catch (err) {
  757. $toast.error(err.message || "해지 처리 중 오류가 발생했습니다.");
  758. processing.value = false;
  759. }
  760. };
  761. // 유틸리티 함수들
  762. const getCategoryText = (category) => {
  763. const categoryMap = {
  764. FASHION_BEAUTY: "패션·뷰티",
  765. FOOD_HEALTH: "식품·건강",
  766. LIFESTYLE: "라이프스타일",
  767. TECH_ELECTRONICS: "테크·가전",
  768. SPORTS_LEISURE: "스포츠·레저",
  769. CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
  770. };
  771. return categoryMap[category] || category || "기타";
  772. };
  773. const getStatusText = (status) => {
  774. const statusMap = {
  775. PENDING: "대기중",
  776. APPROVED: "승인완료",
  777. REJECTED: "거절됨",
  778. TERMINATED: "해지됨",
  779. EXPIRED: "만료됨",
  780. };
  781. return statusMap[status] || status || "알 수 없음";
  782. };
  783. const getStatusColor = (status) => {
  784. const colorMap = {
  785. PENDING: "orange",
  786. APPROVED: "success",
  787. REJECTED: "error",
  788. TERMINATED: "warning",
  789. EXPIRED: "grey",
  790. };
  791. return colorMap[status] || "grey";
  792. };
  793. const getRequestStatusClass = (status) => {
  794. return `request-status-${status?.toLowerCase() || "unknown"}`;
  795. };
  796. const formatDate = (dateString) => {
  797. return new Date(dateString).toLocaleDateString("ko-KR");
  798. };
  799. const formatNumber = (num) => {
  800. if (!num) return "0";
  801. if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
  802. if (num >= 1000) return (num / 1000).toFixed(1) + "K";
  803. return num.toString();
  804. };
  805. const parseSnsChannels = (snsChannels) => {
  806. try {
  807. return JSON.parse(snsChannels);
  808. } catch (e) {
  809. return [];
  810. }
  811. };
  812. const getSnsIcon = (platform) => {
  813. const iconMap = {
  814. instagram: 'mdi-instagram',
  815. youtube: 'mdi-youtube',
  816. tiktok: 'mdi-music-note',
  817. blog: 'mdi-post',
  818. facebook: 'mdi-facebook',
  819. twitter: 'mdi-twitter'
  820. };
  821. return iconMap[platform.toLowerCase()] || 'mdi-link';
  822. };
  823. /************************************************************************
  824. | 라이프사이클
  825. ************************************************************************/
  826. onMounted(async () => {
  827. console.log('🚀 influencer-requests 컴포넌트 마운트됨');
  828. await loadRequests();
  829. });
  830. </script>
  831. <style scoped>
  832. .stats--cards--wrap {
  833. display: grid;
  834. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  835. gap: 20px;
  836. margin-bottom: 30px;
  837. }
  838. .stats--card {
  839. background: white;
  840. border-radius: 12px;
  841. padding: 20px;
  842. display: flex;
  843. align-items: center;
  844. gap: 16px;
  845. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  846. }
  847. .stats--icon {
  848. width: 50px;
  849. height: 50px;
  850. border-radius: 10px;
  851. display: flex;
  852. align-items: center;
  853. justify-content: center;
  854. color: white;
  855. }
  856. .stats--icon.pending {
  857. background: #ff9800;
  858. }
  859. .stats--icon.approved {
  860. background: #4caf50;
  861. }
  862. .stats--icon.rejected {
  863. background: #f44336;
  864. }
  865. .stats--icon.total {
  866. background: #2196f3;
  867. }
  868. .stats--content h3 {
  869. margin: 0;
  870. font-size: 24px;
  871. font-weight: 700;
  872. color: #333;
  873. }
  874. .stats--content p {
  875. margin: 0;
  876. font-size: 14px;
  877. color: #666;
  878. }
  879. .requests--list--wrap {
  880. margin-top: 20px;
  881. }
  882. .requests--grid {
  883. display: grid;
  884. grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
  885. gap: 20px;
  886. margin-bottom: 20px;
  887. }
  888. .request--card {
  889. background: white;
  890. border-radius: 12px;
  891. padding: 20px;
  892. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  893. transition: transform 0.2s, box-shadow 0.2s;
  894. border-left: 4px solid #e0e0e0;
  895. }
  896. .request--card:hover {
  897. transform: translateY(-2px);
  898. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  899. }
  900. .request--card.request-status-pending {
  901. border-left-color: #ff9800;
  902. }
  903. .request--card.request-status-approved {
  904. border-left-color: #4caf50;
  905. }
  906. .request--card.request-status-rejected {
  907. border-left-color: #f44336;
  908. }
  909. .request--card.request-status-terminated {
  910. border-left-color: #ff9800;
  911. }
  912. .request--card--header {
  913. display: flex;
  914. justify-content: space-between;
  915. align-items: flex-start;
  916. margin-bottom: 16px;
  917. }
  918. .influencer--info {
  919. display: flex;
  920. gap: 12px;
  921. flex: 1;
  922. }
  923. .influencer--avatar {
  924. width: 50px;
  925. height: 50px;
  926. border-radius: 50%;
  927. overflow: hidden;
  928. flex-shrink: 0;
  929. display: flex;
  930. align-items: center;
  931. justify-content: center;
  932. background: #f5f5f5;
  933. }
  934. .no-avatar {
  935. font-size: 20px;
  936. font-weight: bold;
  937. color: #666;
  938. }
  939. .influencer--details h4 {
  940. margin: 0 0 4px 0;
  941. font-size: 16px;
  942. font-weight: 600;
  943. }
  944. .influencer--category {
  945. color: #666;
  946. font-size: 14px;
  947. margin: 0 0 8px 0;
  948. }
  949. .influencer--meta {
  950. display: flex;
  951. flex-direction: column;
  952. gap: 2px;
  953. }
  954. .influencer--meta span {
  955. font-size: 12px;
  956. color: #888;
  957. }
  958. .request--status {
  959. text-align: right;
  960. flex-shrink: 0;
  961. }
  962. .request--date {
  963. margin: 8px 0 0;
  964. font-size: 12px;
  965. color: #999;
  966. }
  967. .request--card--body {
  968. margin-bottom: 16px;
  969. }
  970. .request--message,
  971. .request--conditions {
  972. margin-bottom: 12px;
  973. }
  974. .request--message h5,
  975. .request--conditions h5 {
  976. margin: 0 0 8px 0;
  977. font-size: 14px;
  978. font-weight: 600;
  979. color: #333;
  980. }
  981. .request--message p {
  982. margin: 0;
  983. font-size: 14px;
  984. color: #666;
  985. font-style: italic;
  986. }
  987. .condition--item {
  988. display: flex;
  989. gap: 8px;
  990. margin-bottom: 4px;
  991. }
  992. .condition--label {
  993. font-size: 13px;
  994. color: #666;
  995. min-width: 80px;
  996. }
  997. .condition--value {
  998. font-size: 13px;
  999. color: #333;
  1000. font-weight: 500;
  1001. }
  1002. .expire--info {
  1003. display: flex;
  1004. align-items: center;
  1005. gap: 6px;
  1006. font-size: 12px;
  1007. color: #ff9800;
  1008. background: #fff8e1;
  1009. padding: 6px 10px;
  1010. border-radius: 6px;
  1011. }
  1012. .request--card--footer {
  1013. border-top: 1px solid #f0f0f0;
  1014. padding-top: 16px;
  1015. }
  1016. .card--actions {
  1017. display: flex;
  1018. justify-content: space-between;
  1019. align-items: center;
  1020. }
  1021. .approval--actions,
  1022. .approved--actions {
  1023. display: flex;
  1024. gap: 8px;
  1025. }
  1026. .loading-wrap,
  1027. .error-wrap,
  1028. .no-data-wrap {
  1029. display: flex;
  1030. flex-direction: column;
  1031. align-items: center;
  1032. justify-content: center;
  1033. padding: 60px 20px;
  1034. }
  1035. .no-data {
  1036. text-align: center;
  1037. }
  1038. .no-data h3 {
  1039. margin: 16px 0 8px;
  1040. color: #666;
  1041. }
  1042. .no-data p {
  1043. color: #999;
  1044. }
  1045. .pagination-wrap {
  1046. display: flex;
  1047. justify-content: center;
  1048. margin-top: 20px;
  1049. }
  1050. .approve--content,
  1051. .reject--content {
  1052. padding: 8px 0;
  1053. }
  1054. .influencer--summary {
  1055. display: flex;
  1056. align-items: center;
  1057. gap: 12px;
  1058. padding: 12px;
  1059. background: #f8f9fa;
  1060. border-radius: 8px;
  1061. margin-bottom: 16px;
  1062. }
  1063. .influencer--avatar--small {
  1064. width: 40px;
  1065. height: 40px;
  1066. border-radius: 50%;
  1067. overflow: hidden;
  1068. flex-shrink: 0;
  1069. display: flex;
  1070. align-items: center;
  1071. justify-content: center;
  1072. background: #f5f5f5;
  1073. }
  1074. .no-avatar--small {
  1075. font-size: 16px;
  1076. font-weight: bold;
  1077. color: #666;
  1078. }
  1079. .result-count {
  1080. font-size: 14px;
  1081. color: #666;
  1082. font-weight: 500;
  1083. }
  1084. .influencer--contact {
  1085. margin: 4px 0;
  1086. }
  1087. .contact--item {
  1088. display: flex;
  1089. align-items: center;
  1090. gap: 6px;
  1091. font-size: 13px;
  1092. color: #666;
  1093. margin: 2px 0;
  1094. }
  1095. .contact--item .v-icon {
  1096. color: #999;
  1097. }
  1098. .influencer--header {
  1099. margin-bottom: 8px;
  1100. }
  1101. .influencer--contact {
  1102. margin: 8px 0;
  1103. padding: 8px;
  1104. background: #f8f9fa;
  1105. border-radius: 6px;
  1106. }
  1107. .contact--item {
  1108. display: flex;
  1109. align-items: center;
  1110. gap: 6px;
  1111. font-size: 13px;
  1112. color: #666;
  1113. margin: 4px 0;
  1114. }
  1115. .influencer--meta {
  1116. display: flex;
  1117. flex-wrap: wrap;
  1118. gap: 12px;
  1119. margin: 8px 0;
  1120. }
  1121. .meta--item {
  1122. display: flex;
  1123. align-items: center;
  1124. gap: 4px;
  1125. font-size: 13px;
  1126. color: #555;
  1127. background: #f0f0f0;
  1128. padding: 4px 8px;
  1129. border-radius: 4px;
  1130. }
  1131. .influencer--description {
  1132. margin: 8px 0;
  1133. font-size: 13px;
  1134. color: #666;
  1135. line-height: 1.4;
  1136. }
  1137. .influencer--sns {
  1138. display: flex;
  1139. flex-wrap: wrap;
  1140. gap: 8px;
  1141. margin-top: 8px;
  1142. }
  1143. .sns--item {
  1144. display: flex;
  1145. align-items: center;
  1146. gap: 4px;
  1147. font-size: 12px;
  1148. color: #555;
  1149. background: #eef2ff;
  1150. padding: 4px 8px;
  1151. border-radius: 4px;
  1152. }
  1153. /* 해지 버튼 전용 스타일 */
  1154. .btn-terminate {
  1155. background: linear-gradient(135deg, #ff4757 0%, #ff3742 100%) !important;
  1156. color: white !important;
  1157. font-weight: 600 !important;
  1158. border: 2px solid #ff4757 !important;
  1159. box-shadow: 0 2px 8px rgba(255, 71, 87, 0.3) !important;
  1160. transition: all 0.3s ease !important;
  1161. text-transform: none !important;
  1162. letter-spacing: 0.5px !important;
  1163. }
  1164. .btn-terminate:hover {
  1165. background: linear-gradient(135deg, #ff3742 0%, #ff2f3a 100%) !important;
  1166. border-color: #ff3742 !important;
  1167. box-shadow: 0 4px 12px rgba(255, 71, 87, 0.4) !important;
  1168. transform: translateY(-1px) !important;
  1169. }
  1170. .btn-terminate:active {
  1171. transform: translateY(0) !important;
  1172. box-shadow: 0 2px 6px rgba(255, 71, 87, 0.4) !important;
  1173. }
  1174. .btn-terminate .v-icon {
  1175. color: white !important;
  1176. margin-right: 4px !important;
  1177. }
  1178. .btn-terminate:disabled {
  1179. background: #ffbdc1 !important;
  1180. color: #999 !important;
  1181. border-color: #ffbdc1 !important;
  1182. box-shadow: none !important;
  1183. transform: none !important;
  1184. }
  1185. /* 해지 확인 모달 버튼 스타일 */
  1186. .btn-terminate-confirm {
  1187. background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
  1188. color: white !important;
  1189. font-weight: 700 !important;
  1190. font-size: 14px !important;
  1191. padding: 12px 24px !important;
  1192. border-radius: 8px !important;
  1193. border: none !important;
  1194. box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4) !important;
  1195. transition: all 0.3s ease !important;
  1196. text-transform: none !important;
  1197. letter-spacing: 0.5px !important;
  1198. min-width: 120px !important;
  1199. }
  1200. .btn-terminate-confirm:hover {
  1201. background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%) !important;
  1202. box-shadow: 0 6px 16px rgba(220, 38, 38, 0.5) !important;
  1203. transform: translateY(-2px) !important;
  1204. }
  1205. .btn-terminate-confirm:active {
  1206. transform: translateY(0) !important;
  1207. box-shadow: 0 3px 8px rgba(220, 38, 38, 0.4) !important;
  1208. }
  1209. .btn-terminate-confirm .v-icon {
  1210. color: white !important;
  1211. margin-right: 6px !important;
  1212. }
  1213. .btn-terminate-confirm:disabled {
  1214. background: #fca5a5 !important;
  1215. color: #9ca3af !important;
  1216. box-shadow: none !important;
  1217. transform: none !important;
  1218. }
  1219. </style>