search.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  1. <template>
  2. <div>
  3. <div class="inner--headers">
  4. <h2>{{ pageId }}</h2>
  5. <div class="bread--crumbs--wrap">
  6. <span>홈</span>
  7. <span>{{ pageId }}</span>
  8. </div>
  9. </div>
  10. <!-- 검색 및 필터 영역 -->
  11. <div class="search--modules type2">
  12. <div class="search--inner">
  13. <div class="form--cont--filter">
  14. <v-select
  15. v-model="searchFilter.category"
  16. :items="categoryOptions"
  17. variant="outlined"
  18. class="custom-select"
  19. label="카테고리"
  20. clearable
  21. >
  22. </v-select>
  23. </div>
  24. <div class="form--cont--filter">
  25. <v-select
  26. v-model="searchFilter.region"
  27. :items="regionOptions"
  28. variant="outlined"
  29. class="custom-select"
  30. label="지역"
  31. clearable
  32. >
  33. </v-select>
  34. </div>
  35. <div class="form--cont--text">
  36. <v-text-field
  37. v-model="searchFilter.keyword"
  38. class="custom-input mini"
  39. style="width: 100%"
  40. placeholder="벤더사명을 입력하세요"
  41. @keyup.enter="handleSearch"
  42. ></v-text-field>
  43. </div>
  44. </div>
  45. <v-btn
  46. class="custom-btn btn-blue mini sch--btn"
  47. @click="handleSearch"
  48. :loading="loading"
  49. >
  50. 검색
  51. </v-btn>
  52. </div>
  53. <!-- 내 승인 요청 현황 -->
  54. <div class="data--list--wrap mb-4">
  55. <div class="section--header">
  56. <h3>내 승인 요청 현황</h3>
  57. <v-btn
  58. class="custom-btn mini btn-outline"
  59. @click="showMyRequests = !showMyRequests"
  60. >
  61. {{ showMyRequests ? "숨기기" : "보기" }}
  62. </v-btn>
  63. </div>
  64. <div v-show="showMyRequests" class="my--requests--wrap">
  65. <div v-if="myRequests.length === 0" class="no-data">
  66. <p>진행 중인 승인 요청이 없습니다.</p>
  67. </div>
  68. <div v-else class="request--cards">
  69. <div
  70. v-for="request in myRequests"
  71. :key="request.SEQ"
  72. class="request--card"
  73. :class="getStatusClass(request.STATUS)"
  74. >
  75. <div class="card--header">
  76. <h4>{{ request.vendorName }}</h4>
  77. <v-chip :color="getStatusColor(request.STATUS)" size="small">
  78. {{ getStatusText(request.STATUS) }}
  79. </v-chip>
  80. </div>
  81. <div class="card--content">
  82. <p class="request--date">요청일: {{ formatDate(request.REQUEST_DATE) }}</p>
  83. <p v-if="request.STATUS === 'PENDING'" class="expire--date">
  84. 만료일: {{ formatDate(request.EXPIRED_DATE) }}
  85. </p>
  86. <p v-if="request.REQUEST_MESSAGE" class="request--message">
  87. "{{ request.REQUEST_MESSAGE }}"
  88. </p>
  89. </div>
  90. <div class="card--actions">
  91. <v-btn
  92. v-if="request.STATUS === 'PENDING'"
  93. class="custom-btn mini btn-outline-red"
  94. @click="cancelRequest(request.SEQ)"
  95. size="small"
  96. >
  97. 취소
  98. </v-btn>
  99. <v-btn
  100. class="custom-btn mini btn-outline"
  101. @click="viewRequestDetail(request.SEQ)"
  102. size="small"
  103. >
  104. 상세보기
  105. </v-btn>
  106. </div>
  107. </div>
  108. </div>
  109. </div>
  110. </div>
  111. <!-- 벤더사 검색 결과 -->
  112. <div class="data--list--wrap">
  113. <div class="btn--actions--wrap">
  114. <div class="left--sections">
  115. <span class="result-count">
  116. 총 {{ pagination.totalCount || 0 }}개의 벤더사
  117. </span>
  118. </div>
  119. <div class="right--sections">
  120. <v-select
  121. v-model="sortOption"
  122. :items="sortOptions"
  123. variant="outlined"
  124. class="custom-select mini"
  125. @update:model-value="handleSort"
  126. >
  127. </v-select>
  128. </div>
  129. </div>
  130. <!-- 로딩 상태 -->
  131. <div v-if="loading" class="loading-wrap">
  132. <v-progress-circular indeterminate color="primary"></v-progress-circular>
  133. <p>벤더사를 검색하고 있습니다...</p>
  134. </div>
  135. <!-- 에러 상태 -->
  136. <div v-else-if="error" class="error-wrap">
  137. <v-alert type="error" dismissible @click:close="error = null">
  138. {{ error }}
  139. </v-alert>
  140. </div>
  141. <!-- 벤더사 리스트 -->
  142. <div v-else-if="vendors.length > 0" class="vendor--search--wrap">
  143. <div class="vendor--grid">
  144. <div v-for="vendor in vendors" :key="vendor.SEQ" class="vendor--card">
  145. <div class="vendor--card--header">
  146. <div class="vendor--logo">
  147. <v-img
  148. v-if="vendor.LOGO"
  149. :src="vendor.LOGO"
  150. :alt="vendor.COMPANY_NAME + ' 로고'"
  151. width="60"
  152. height="60"
  153. ></v-img>
  154. <div v-else class="no-logo">
  155. {{ vendor.COMPANY_NAME?.charAt(0) || "V" }}
  156. </div>
  157. </div>
  158. <div class="vendor--info">
  159. <h3>{{ vendor.COMPANY_NAME }}</h3>
  160. <p class="vendor--category">{{ getCategoryText(vendor.CATEGORY) }}</p>
  161. <div class="vendor--meta">
  162. <span v-if="vendor.REGION">📍 {{ vendor.REGION }}</span>
  163. <span v-if="vendor.PARTNERSHIP_COUNT"
  164. >🤝 {{ vendor.PARTNERSHIP_COUNT }}개 파트너십</span
  165. >
  166. </div>
  167. </div>
  168. </div>
  169. <div class="vendor--card--body">
  170. <p v-if="vendor.DESCRIPTION" class="vendor--description">
  171. {{ vendor.DESCRIPTION }}
  172. </p>
  173. <div class="vendor--tags" v-if="vendor.TAGS">
  174. <v-chip
  175. v-for="tag in vendor.TAGS.split(',')"
  176. :key="tag"
  177. size="small"
  178. variant="outlined"
  179. class="mr-1 mb-1"
  180. >
  181. {{ tag.trim() }}
  182. </v-chip>
  183. </div>
  184. </div>
  185. <div class="vendor--card--footer">
  186. <div class="partnership--status">
  187. <span
  188. v-if="getPartnershipStatus(vendor.SEQ)"
  189. :class="[
  190. 'status-badge',
  191. getPartnershipStatus(vendor.SEQ)?.toLowerCase() || 'unknown',
  192. ]"
  193. >
  194. {{ getPartnershipStatusText(vendor.SEQ) }}
  195. </span>
  196. <span v-else class="status-badge available">신규 파트너십 가능</span>
  197. </div>
  198. <div class="card--actions">
  199. <v-btn
  200. class="custom-btn mini btn-outline mr-2"
  201. @click="viewVendorDetail(vendor.SEQ)"
  202. >
  203. 상세보기
  204. </v-btn>
  205. <v-btn
  206. v-if="showRequestButton(vendor.SEQ)"
  207. class="custom-btn mini btn-blue"
  208. @click="openRequestModal(vendor)"
  209. >
  210. 승인요청
  211. </v-btn>
  212. <v-chip
  213. v-else-if="getPartnershipStatus(vendor.SEQ) === 'APPROVED'"
  214. color="success"
  215. size="small"
  216. >
  217. 승인완료
  218. </v-chip>
  219. </div>
  220. </div>
  221. </div>
  222. </div>
  223. <!-- 페이지네이션 -->
  224. <div class="pagination-wrap" v-if="pagination.totalPages > 1">
  225. <v-pagination
  226. v-model="currentPage"
  227. :length="pagination.totalPages"
  228. :total-visible="7"
  229. @update:model-value="handlePageChange"
  230. ></v-pagination>
  231. </div>
  232. </div>
  233. <!-- 검색 결과 없음 -->
  234. <div v-else class="no-data-wrap">
  235. <div class="no-data">
  236. <v-icon size="64" color="grey-lighten-1">mdi-store-search</v-icon>
  237. <h3>검색된 벤더사가 없습니다</h3>
  238. <p>다른 검색 조건을 시도해보세요</p>
  239. </div>
  240. </div>
  241. </div>
  242. <!-- 승인요청 모달 -->
  243. <v-dialog v-model="requestModal.show" max-width="600px">
  244. <v-card>
  245. <v-card-title class="text-h5">
  246. {{ requestModal.vendor?.COMPANY_NAME }}에 승인요청
  247. </v-card-title>
  248. <v-card-text>
  249. <div class="request--form">
  250. <div class="vendor--summary">
  251. <div class="vendor--logo--small">
  252. <v-img
  253. v-if="requestModal.vendor?.LOGO"
  254. :src="requestModal.vendor.LOGO"
  255. width="40"
  256. height="40"
  257. ></v-img>
  258. <div v-else class="no-logo--small">
  259. {{ requestModal.vendor?.COMPANY_NAME?.charAt(0) || "V" }}
  260. </div>
  261. </div>
  262. <div>
  263. <h4>{{ requestModal.vendor?.COMPANY_NAME }}</h4>
  264. <p>{{ getCategoryText(requestModal.vendor?.CATEGORY) }}</p>
  265. </div>
  266. </div>
  267. <v-textarea
  268. v-model="requestModal.message"
  269. label="요청 메시지"
  270. placeholder="벤더사에 전달할 메시지를 작성해주세요..."
  271. rows="4"
  272. counter="500"
  273. maxlength="500"
  274. class="mt-4"
  275. ></v-textarea>
  276. <div class="form--section">
  277. <h5>희망 조건 (선택사항)</h5>
  278. <v-text-field
  279. v-model="requestModal.commissionRate"
  280. label="희망 수수료율 (%)"
  281. type="number"
  282. min="0"
  283. max="100"
  284. step="0.1"
  285. class="mt-2"
  286. ></v-text-field>
  287. <v-textarea
  288. v-model="requestModal.specialConditions"
  289. label="특별 조건"
  290. placeholder="기타 협업 조건이나 요청사항을 입력해주세요..."
  291. rows="3"
  292. class="mt-2"
  293. ></v-textarea>
  294. </div>
  295. </div>
  296. </v-card-text>
  297. <v-card-actions>
  298. <v-spacer></v-spacer>
  299. <v-btn color="grey" variant="text" @click="closeRequestModal"> 취소 </v-btn>
  300. <v-btn color="primary" @click="submitRequest" :loading="submitting">
  301. 승인요청
  302. </v-btn>
  303. </v-card-actions>
  304. </v-card>
  305. </v-dialog>
  306. </div>
  307. </template>
  308. <script setup>
  309. import { ref, onMounted, computed } from "vue";
  310. import { useRouter } from "vue-router";
  311. /************************************************************************
  312. | 레이아웃
  313. ************************************************************************/
  314. definePageMeta({
  315. layout: "default",
  316. });
  317. /************************************************************************
  318. | 스토어 & 라우터
  319. ************************************************************************/
  320. const router = useRouter();
  321. const { $toast } = useNuxtApp();
  322. /************************************************************************
  323. | 반응형 데이터
  324. ************************************************************************/
  325. const pageId = ref("벤더사 검색");
  326. const loading = ref(false);
  327. const submitting = ref(false);
  328. const error = ref(null);
  329. const currentPage = ref(1);
  330. const showMyRequests = ref(false);
  331. // 검색 필터
  332. const searchFilter = ref({
  333. keyword: "",
  334. category: "",
  335. region: "",
  336. });
  337. // 정렬 옵션
  338. const sortOption = ref("latest");
  339. const sortOptions = ref([
  340. { title: "최신순", value: "latest" },
  341. { title: "파트너십 많은순", value: "partnership" },
  342. { title: "이름순", value: "name" },
  343. ]);
  344. // 카테고리 옵션
  345. const categoryOptions = ref([
  346. { title: "전체", value: "" },
  347. { title: "패션·뷰티", value: "FASHION_BEAUTY" },
  348. { title: "식품·건강", value: "FOOD_HEALTH" },
  349. { title: "라이프스타일", value: "LIFESTYLE" },
  350. { title: "테크·가전", value: "TECH_ELECTRONICS" },
  351. { title: "스포츠·레저", value: "SPORTS_LEISURE" },
  352. { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" },
  353. ]);
  354. // 지역 옵션
  355. const regionOptions = ref([
  356. { title: "전체", value: "" },
  357. { title: "서울", value: "SEOUL" },
  358. { title: "경기", value: "GYEONGGI" },
  359. { title: "인천", value: "INCHEON" },
  360. { title: "부산", value: "BUSAN" },
  361. { title: "대구", value: "DAEGU" },
  362. { title: "대전", value: "DAEJEON" },
  363. { title: "광주", value: "GWANGJU" },
  364. { title: "울산", value: "ULSAN" },
  365. { title: "기타", value: "OTHER" },
  366. ]);
  367. // 데이터
  368. const vendors = ref([]);
  369. const myRequests = ref([]);
  370. const pagination = ref({
  371. currentPage: 1,
  372. totalPages: 1,
  373. totalCount: 0,
  374. pageSize: 12,
  375. });
  376. // 승인요청 모달
  377. const requestModal = ref({
  378. show: false,
  379. vendor: null,
  380. message: "",
  381. commissionRate: null,
  382. specialConditions: "",
  383. });
  384. /************************************************************************
  385. | computed
  386. ************************************************************************/
  387. const currentUser = computed(() => {
  388. // 인증 스토어에서 사용자 정보 가져오기
  389. const authStore = useAuthStore();
  390. console.log('🔍 currentUser computed 디버깅:', {
  391. 'authStore.auth': authStore.auth,
  392. 'snsTempData': authStore.auth.snsTempData,
  393. 'localStorage': localStorage.getItem("authStore")
  394. });
  395. // SNS 로그인 시 snsTempData가 있는 경우 해당 데이터 사용
  396. if (authStore.auth.snsTempData?.user) {
  397. console.log('✅ SNS 로그인 데이터 사용:', authStore.auth.snsTempData.user);
  398. return authStore.auth.snsTempData.user;
  399. }
  400. // 일반 로그인 시 auth 데이터 사용
  401. if (authStore.auth.seq) {
  402. console.log('✅ 일반 로그인 데이터 사용:', authStore.auth);
  403. return authStore.auth;
  404. }
  405. // 로컬스토리지 백업
  406. try {
  407. const localAuthData = JSON.parse(localStorage.getItem("authStore"))?.auth;
  408. console.log('💾 localStorage 데이터:', localAuthData);
  409. if (localAuthData?.snsTempData?.user) {
  410. console.log('✅ localStorage SNS 데이터 사용:', localAuthData.snsTempData.user);
  411. return localAuthData.snsTempData.user;
  412. }
  413. console.log('✅ localStorage 일반 데이터 사용:', localAuthData);
  414. return localAuthData || {};
  415. } catch (e) {
  416. console.error('❌ localStorage authStore 파싱 오류:', e);
  417. return {};
  418. }
  419. });
  420. /************************************************************************
  421. | 메서드
  422. ************************************************************************/
  423. const handleSearch = async () => {
  424. currentPage.value = 1;
  425. await loadVendors();
  426. };
  427. const handlePageChange = async (page) => {
  428. currentPage.value = page;
  429. await loadVendors();
  430. };
  431. const handleSort = async () => {
  432. currentPage.value = 1;
  433. await loadVendors();
  434. };
  435. const loadVendors = async () => {
  436. try {
  437. loading.value = true;
  438. error.value = null;
  439. const params = {
  440. keyword: searchFilter.value.keyword,
  441. category: searchFilter.value.category,
  442. region: searchFilter.value.region,
  443. sortBy: sortOption.value,
  444. page: currentPage.value,
  445. size: pagination.value.pageSize,
  446. influencerSeq: currentUser.value.SEQ || currentUser.value.seq || currentUser.value.ID || currentUser.value.id,
  447. };
  448. useAxios()
  449. .post("/api/vendor/search", params)
  450. .then((res) => {
  451. if (res.data.success) {
  452. vendors.value = res.data.data.items;
  453. pagination.value = res.data.data.pagination;
  454. } else {
  455. error.value = res.data.message || "벤더사 검색 중 오류가 발생했습니다.";
  456. }
  457. })
  458. .catch((err) => {
  459. error.value = err.message || "벤더사 검색 중 오류가 발생했습니다.";
  460. })
  461. .finally(() => {
  462. loading.value = false;
  463. });
  464. } catch (err) {
  465. error.value = err.message || "벤더사 검색 중 오류가 발생했습니다.";
  466. loading.value = false;
  467. }
  468. };
  469. const loadMyRequests = async () => {
  470. try {
  471. const params = {
  472. influencerSeq: currentUser.value.SEQ || currentUser.value.seq || currentUser.value.ID || currentUser.value.id,
  473. // status 파라미터 제거하여 모든 상태의 요청을 로드
  474. };
  475. useAxios()
  476. .post("/api/vendor-influencer/list", params)
  477. .then((res) => {
  478. if (res.data.success) {
  479. myRequests.value = res.data.data.items;
  480. }
  481. })
  482. .catch((err) => {
  483. console.error("내 요청 목록 로드 오류:", err);
  484. });
  485. } catch (err) {
  486. console.error("내 요청 목록 로드 오류:", err);
  487. }
  488. };
  489. const openRequestModal = (vendor) => {
  490. requestModal.value = {
  491. show: true,
  492. vendor: vendor,
  493. message: "",
  494. commissionRate: null,
  495. specialConditions: "",
  496. };
  497. };
  498. const closeRequestModal = () => {
  499. requestModal.value = {
  500. show: false,
  501. vendor: null,
  502. message: "",
  503. commissionRate: null,
  504. specialConditions: "",
  505. };
  506. };
  507. const submitRequest = async () => {
  508. try {
  509. submitting.value = true;
  510. // 사용자 seq 필드 확인 (SNS 로그인 시 대문자 SEQ, 일반 로그인 시 소문자 seq)
  511. console.log('🔍 currentUser.value 전체:', currentUser.value);
  512. console.log('🔍 SEQ 후보들:', {
  513. 'SEQ': currentUser.value.SEQ,
  514. 'seq': currentUser.value.seq,
  515. 'id': currentUser.value.id,
  516. 'ID': currentUser.value.ID
  517. });
  518. const userSeq = currentUser.value.SEQ || currentUser.value.seq || currentUser.value.ID || currentUser.value.id;
  519. console.log('🎯 최종 선택된 userSeq:', userSeq);
  520. if (!userSeq) {
  521. console.error('❌ userSeq가 비어있음:', currentUser.value);
  522. $toast.error('로그인 정보를 확인할 수 없습니다. 다시 로그인해주세요.');
  523. return;
  524. }
  525. const params = {
  526. vendorSeq: requestModal.value.vendor.SEQ,
  527. influencerSeq: userSeq,
  528. requestType: "INFLUENCER_REQUEST",
  529. requestMessage: requestModal.value.message,
  530. requestedBy: userSeq,
  531. commissionRate: requestModal.value.commissionRate,
  532. specialConditions: requestModal.value.specialConditions,
  533. };
  534. console.log('📤 API 호출 파라미터:', params);
  535. useAxios()
  536. .post("/api/vendor-influencer/request", params)
  537. .then((res) => {
  538. if (res.data.success) {
  539. $toast.success("승인요청이 성공적으로 전송되었습니다.");
  540. closeRequestModal();
  541. loadMyRequests();
  542. loadVendors();
  543. } else {
  544. $toast.error(res.data.message || "승인요청 전송 중 오류가 발생했습니다.");
  545. }
  546. })
  547. .catch((err) => {
  548. $toast.error(err.message || "승인요청 전송 중 오류가 발생했습니다.");
  549. })
  550. .finally(() => {
  551. submitting.value = false;
  552. });
  553. } catch (err) {
  554. $toast.error(err.message || "승인요청 전송 중 오류가 발생했습니다.");
  555. submitting.value = false;
  556. }
  557. };
  558. const cancelRequest = async (requestSeq) => {
  559. if (!confirm("승인요청을 취소하시겠습니까?")) return;
  560. try {
  561. const params = {
  562. mappingSeq: requestSeq,
  563. cancelledBy: currentUser.value.SEQ || currentUser.value.seq || currentUser.value.ID || currentUser.value.id,
  564. cancelReason: "사용자에 의한 취소",
  565. };
  566. useAxios()
  567. .post("/api/vendor-influencer/cancel", params)
  568. .then((res) => {
  569. if (res.data.success) {
  570. $toast.success("승인요청이 취소되었습니다.");
  571. loadMyRequests();
  572. } else {
  573. $toast.error(res.data.message || "요청 취소 중 오류가 발생했습니다.");
  574. }
  575. })
  576. .catch((err) => {
  577. $toast.error(err.message || "요청 취소 중 오류가 발생했습니다.");
  578. });
  579. } catch (err) {
  580. $toast.error(err.message || "요청 취소 중 오류가 발생했습니다.");
  581. }
  582. };
  583. const viewVendorDetail = (vendorSeq) => {
  584. router.push(`/view/vendor/${vendorSeq}`);
  585. };
  586. const viewRequestDetail = (requestSeq) => {
  587. router.push(`/view/vendor/request/${requestSeq}`);
  588. };
  589. // 유틸리티 함수들
  590. const getCategoryText = (category) => {
  591. const categoryMap = {
  592. FASHION_BEAUTY: "패션·뷰티",
  593. FOOD_HEALTH: "식품·건강",
  594. LIFESTYLE: "라이프스타일",
  595. TECH_ELECTRONICS: "테크·가전",
  596. SPORTS_LEISURE: "스포츠·레저",
  597. CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
  598. };
  599. return categoryMap[category] || category || "기타";
  600. };
  601. const getStatusText = (status) => {
  602. const statusMap = {
  603. PENDING: "대기중",
  604. APPROVED: "승인완료",
  605. REJECTED: "거절됨",
  606. CANCELLED: "취소됨",
  607. };
  608. return statusMap[status] || status || "알 수 없음";
  609. };
  610. const getStatusColor = (status) => {
  611. const colorMap = {
  612. PENDING: "orange",
  613. APPROVED: "success",
  614. REJECTED: "error",
  615. CANCELLED: "grey",
  616. };
  617. return colorMap[status] || "grey";
  618. };
  619. const getStatusClass = (status) => {
  620. return `status-${status?.toLowerCase() || "unknown"}`;
  621. };
  622. const getPartnershipStatus = (vendorSeq) => {
  623. // 현재 인플루언서의 해당 벤더사에 대한 파트너십 상태 확인
  624. const request = myRequests.value.find(req => req.VENDOR_SEQ === vendorSeq);
  625. return request ? request.STATUS : null;
  626. };
  627. const getPartnershipStatusText = (vendorSeq) => {
  628. const status = getPartnershipStatus(vendorSeq);
  629. const statusMap = {
  630. PENDING: "승인 대기중",
  631. APPROVED: "승인 완료",
  632. REJECTED: "승인 거절됨",
  633. CANCELLED: "요청 취소됨"
  634. };
  635. return statusMap[status] || "신규 파트너십 가능";
  636. };
  637. // 승인요청 버튼 표시 여부 확인
  638. const showRequestButton = (vendorSeq) => {
  639. const status = getPartnershipStatus(vendorSeq);
  640. return !status || status === 'REJECTED' || status === 'CANCELLED';
  641. };
  642. const formatDate = (dateString) => {
  643. return new Date(dateString).toLocaleDateString("ko-KR");
  644. };
  645. /************************************************************************
  646. | 라이프사이클
  647. ************************************************************************/
  648. onMounted(async () => {
  649. await Promise.all([loadVendors(), loadMyRequests()]);
  650. });
  651. </script>
  652. <style scoped>
  653. .section--header {
  654. display: flex;
  655. justify-content: space-between;
  656. align-items: center;
  657. margin-bottom: 16px;
  658. padding-bottom: 8px;
  659. border-bottom: 1px solid #e0e0e0;
  660. }
  661. .my--requests--wrap {
  662. background: #f8f9fa;
  663. border-radius: 8px;
  664. padding: 16px;
  665. margin-bottom: 20px;
  666. }
  667. .request--cards {
  668. display: grid;
  669. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  670. gap: 16px;
  671. }
  672. .request--card {
  673. background: white;
  674. border-radius: 8px;
  675. padding: 16px;
  676. border-left: 4px solid #e0e0e0;
  677. }
  678. .request--card.status-pending {
  679. border-left-color: #ff9800;
  680. }
  681. .request--card.status-approved {
  682. border-left-color: #4caf50;
  683. }
  684. .request--card.status-rejected {
  685. border-left-color: #f44336;
  686. }
  687. .card--header {
  688. display: flex;
  689. justify-content: space-between;
  690. align-items: center;
  691. margin-bottom: 12px;
  692. }
  693. .card--header h4 {
  694. margin: 0;
  695. font-size: 16px;
  696. font-weight: 600;
  697. }
  698. .card--content {
  699. margin-bottom: 12px;
  700. }
  701. .card--content p {
  702. margin: 4px 0;
  703. font-size: 14px;
  704. color: #666;
  705. }
  706. .request--message {
  707. font-style: italic;
  708. color: #333 !important;
  709. }
  710. .card--actions {
  711. display: flex;
  712. gap: 8px;
  713. }
  714. .vendor--search--wrap {
  715. margin-top: 20px;
  716. }
  717. .vendor--grid {
  718. display: grid;
  719. grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
  720. gap: 20px;
  721. margin-bottom: 20px;
  722. }
  723. .vendor--card {
  724. background: white;
  725. border-radius: 12px;
  726. padding: 20px;
  727. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  728. transition: transform 0.2s, box-shadow 0.2s;
  729. }
  730. .vendor--card:hover {
  731. transform: translateY(-2px);
  732. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  733. }
  734. .vendor--card--header {
  735. display: flex;
  736. gap: 16px;
  737. margin-bottom: 16px;
  738. }
  739. .vendor--logo {
  740. width: 60px;
  741. height: 60px;
  742. border-radius: 8px;
  743. overflow: hidden;
  744. flex-shrink: 0;
  745. display: flex;
  746. align-items: center;
  747. justify-content: center;
  748. background: #f5f5f5;
  749. }
  750. .no-logo {
  751. font-size: 24px;
  752. font-weight: bold;
  753. color: #666;
  754. }
  755. .vendor--info h3 {
  756. margin: 0 0 4px 0;
  757. font-size: 18px;
  758. font-weight: 600;
  759. }
  760. .vendor--category {
  761. color: #666;
  762. font-size: 14px;
  763. margin: 0 0 8px 0;
  764. }
  765. .vendor--meta {
  766. display: flex;
  767. flex-direction: column;
  768. gap: 4px;
  769. }
  770. .vendor--meta span {
  771. font-size: 12px;
  772. color: #888;
  773. }
  774. .vendor--card--body {
  775. margin-bottom: 16px;
  776. }
  777. .vendor--description {
  778. font-size: 14px;
  779. color: #666;
  780. line-height: 1.4;
  781. margin-bottom: 12px;
  782. }
  783. .vendor--tags {
  784. margin-bottom: 8px;
  785. }
  786. .vendor--card--footer {
  787. display: flex;
  788. justify-content: space-between;
  789. align-items: center;
  790. }
  791. .partnership--status {
  792. flex: 1;
  793. }
  794. .status-badge {
  795. padding: 4px 8px;
  796. border-radius: 4px;
  797. font-size: 12px;
  798. font-weight: 500;
  799. }
  800. .status-badge.available {
  801. background: #e8f5e8;
  802. color: #2e7d32;
  803. }
  804. .status-badge.pending {
  805. background: #fff3e0;
  806. color: #ef6c00;
  807. }
  808. .status-badge.approved {
  809. background: #e8f5e8;
  810. color: #2e7d32;
  811. }
  812. .status-badge.rejected {
  813. background: #ffebee;
  814. color: #c62828;
  815. }
  816. .card--actions {
  817. display: flex;
  818. gap: 8px;
  819. }
  820. .loading-wrap,
  821. .error-wrap,
  822. .no-data-wrap {
  823. display: flex;
  824. flex-direction: column;
  825. align-items: center;
  826. justify-content: center;
  827. padding: 60px 20px;
  828. }
  829. .no-data {
  830. text-align: center;
  831. }
  832. .no-data h3 {
  833. margin: 16px 0 8px;
  834. color: #666;
  835. }
  836. .no-data p {
  837. color: #999;
  838. }
  839. .pagination-wrap {
  840. display: flex;
  841. justify-content: center;
  842. margin-top: 20px;
  843. }
  844. .request--form {
  845. padding: 8px 0;
  846. }
  847. .vendor--summary {
  848. display: flex;
  849. align-items: center;
  850. gap: 12px;
  851. padding: 12px;
  852. background: #f8f9fa;
  853. border-radius: 8px;
  854. margin-bottom: 16px;
  855. }
  856. .vendor--logo--small {
  857. width: 40px;
  858. height: 40px;
  859. border-radius: 6px;
  860. overflow: hidden;
  861. flex-shrink: 0;
  862. display: flex;
  863. align-items: center;
  864. justify-content: center;
  865. background: #f5f5f5;
  866. }
  867. .no-logo--small {
  868. font-size: 16px;
  869. font-weight: bold;
  870. color: #666;
  871. }
  872. .form--section {
  873. margin-top: 16px;
  874. }
  875. .form--section h5 {
  876. margin: 0 0 8px 0;
  877. font-size: 14px;
  878. font-weight: 600;
  879. color: #333;
  880. }
  881. .result-count {
  882. font-size: 14px;
  883. color: #666;
  884. font-weight: 500;
  885. }
  886. </style>