index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. <template>
  2. <div class="mypage-container">
  3. <!-- 프로필 섹션 -->
  4. <div class="profile-section">
  5. <div class="profile-card">
  6. <div class="profile-header">
  7. <div class="profile-avatar">
  8. {{ getMemberTypeName(userInfo.memberType) === "인플루언서" ? '❤️' : '💜' }}
  9. </div>
  10. <div class="profile-info">
  11. <h2>{{ userInfo.name }}</h2>
  12. <span class="member-type-badge">{{ getMemberTypeName(userInfo.memberType) }}</span>
  13. </div>
  14. <button
  15. class="edit-btn"
  16. @click="toggleEditMode"
  17. :class="{ active: isEditMode }"
  18. >
  19. {{ isEditMode ? '취소' : '수정' }}
  20. </button>
  21. </div>
  22. <!-- 정보 표시/수정 폼 -->
  23. <div class="profile-content">
  24. <div v-if="!isEditMode" class="info-display">
  25. <div class="info-row">
  26. <label>이름</label>
  27. <span>{{ myInfo.name }}</span>
  28. </div>
  29. <div class="info-row" v-if="userInfo.memberType === 'INFLUENCER'">
  30. <label>닉네임</label>
  31. <span>{{ myInfo.nickName }}</span>
  32. </div>
  33. <div class="info-row">
  34. <label>이메일</label>
  35. <span>{{ myInfo.email }}</span>
  36. </div>
  37. <div class="info-row" v-if="userInfo.memberType !== 'INFLUENCER'">
  38. <label>회사명</label>
  39. <span>{{ userInfo.companyName }}</span>
  40. </div>
  41. <div class="info-row">
  42. <label>전화번호</label>
  43. <span>{{ myInfo.phone || '미등록' }}</span>
  44. </div>
  45. <div class="info-row">
  46. <label>SNS</label>
  47. <span>{{ myInfo.sns_type || '미등록' }} {{ myInfo.sns_link || '미등록' }}</span>
  48. </div>
  49. </div>
  50. <div v-else class="info-edit">
  51. <div class="info-row">
  52. <label>이름</label>
  53. <span>{{ myInfo.name }}</span>
  54. </div>
  55. <div class="info-row" v-if="userInfo.memberType === 'INFLUENCER'">
  56. <label>닉네임</label>
  57. <input
  58. type="text"
  59. v-model="editForm.nickName"
  60. placeholder="닉네임을 입력하세요"
  61. class="edit-input w--20"
  62. />
  63. </div>
  64. <div class="info-row">
  65. <label>비밀번호 변경</label>
  66. <input
  67. type="password"
  68. v-model="editForm.newPassword"
  69. placeholder="비밀번호를 입력하세요."
  70. class="edit-input"
  71. />
  72. </div>
  73. <div class="info-row" v-if="editForm.newPassword">
  74. <label>비밀번호 확인</label>
  75. <input
  76. type="password"
  77. v-model="editForm.confirmPassword"
  78. placeholder="비밀번호를 한 번 더 입력하세요."
  79. class="edit-input"
  80. />
  81. </div>
  82. <div class="info-row">
  83. <label>이메일</label>
  84. <input
  85. type="email"
  86. v-model="editForm.email"
  87. placeholder="이메일을 입력하세요"
  88. class="edit-input"
  89. />
  90. </div>
  91. <div class="info-row" v-if="userInfo.memberType !== 'INFLUENCER'">
  92. <label>회사명</label>
  93. <input
  94. type="text"
  95. v-model="editForm.companyName"
  96. placeholder="회사명을 입력하세요"
  97. class="edit-input"
  98. />
  99. </div>
  100. <div class="info-row">
  101. <label>전화번호</label>
  102. <input
  103. type="tel"
  104. v-model="editForm.phone"
  105. placeholder="전화번호를 입력하세요"
  106. class="edit-input"
  107. />
  108. </div>
  109. <div class="info-row sns--row">
  110. <label>SNS</label>
  111. <select v-model="editForm.snsType" class="edit-input">
  112. <option value="">선택하세요</option>
  113. <option value="youtube">유튜브</option>
  114. <option value="instagram">인스타그램</option>
  115. <option value="naver">네이버 블로그</option>
  116. <option value="tiktok">틱톡</option>
  117. <option value="facebook">페이스북</option>
  118. <option value="twitter">트위터</option>
  119. <option value="blog">개인 블로그</option>
  120. </select>
  121. <div class="edit-input-container">
  122. <input
  123. type="text"
  124. v-model="editForm.snsLink"
  125. :placeholder="getSnsPlaceholder(editForm.snsType)"
  126. class="edit-input"
  127. />
  128. </div>
  129. </div>
  130. <div class="edit-actions">
  131. <button
  132. class="btn-save"
  133. @click="fnUpdate"
  134. >
  135. 저장
  136. </button>
  137. <button
  138. class="btn-cancel"
  139. @click="cancelEdit"
  140. >
  141. 취소
  142. </button>
  143. </div>
  144. </div>
  145. </div>
  146. </div>
  147. </div>
  148. <!-- 추가 정보 섹션 -->
  149. <div class="additional-info">
  150. <div class="info-card">
  151. <h3>계정 정보</h3>
  152. <div class="info-grid">
  153. <div class="info-item">
  154. <span class="info-label">계정 타입</span>
  155. <span class="info-value">{{ getMemberTypeName(userInfo.memberType) }}</span>
  156. </div>
  157. <div class="info-item">
  158. <span class="info-label">계정 ID</span>
  159. <span class="info-value">{{ userInfo.id }}</span>
  160. </div>
  161. <div class="info-item">
  162. <span class="info-label">가입일</span>
  163. <span class="info-value">{{ formatDate(myInfo.regDate) || '정보 없음' }}</span>
  164. </div>
  165. </div>
  166. </div>
  167. </div>
  168. </div>
  169. </template>
  170. <script setup>
  171. import "@vuepic/vue-datepicker/dist/main.css";
  172. /************************************************************************
  173. | 레이아웃
  174. ************************************************************************/
  175. definePageMeta({
  176. layout: "default",
  177. });
  178. /************************************************************************
  179. | PROPS
  180. ************************************************************************/
  181. const props = defineProps({
  182. propsData: {
  183. type: Object,
  184. default: () => {},
  185. },
  186. });
  187. /************************************************************************
  188. | 스토어
  189. ************************************************************************/
  190. const useDtStore = useDetailStore();
  191. const useAtStore = useAuthStore();
  192. /************************************************************************
  193. | 전역
  194. ************************************************************************/
  195. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
  196. const router = useRouter();
  197. const myInfo = ref({});
  198. // 사용자 정보
  199. const userInfo = ref({
  200. id: useAtStore.auth.id,
  201. name: useAtStore.auth.name,
  202. email: useAtStore.auth.email,
  203. companyName: useAtStore.auth.companyName,
  204. companyNumber: useAtStore.auth.companyNumber,
  205. phone: useAtStore.auth.phone,
  206. memberType: useAtStore.auth.memberType,
  207. regDate: null
  208. });
  209. // 편집 모드
  210. const isEditMode = ref(false);
  211. const editForm = ref({
  212. name: '',
  213. nickName: '',
  214. email: '',
  215. companyName: '',
  216. phone: '',
  217. snsType: '',
  218. snsLink: '',
  219. newPassword: '',
  220. confirmPassword: ''
  221. });
  222. /* eslint-disable */
  223. /* prettier-ignore */
  224. /************************************************************************
  225. | 함수(METHODS)
  226. ************************************************************************/
  227. // 회원 타입 이름 반환
  228. const getMemberTypeName = (type) => {
  229. switch(type) {
  230. case 'INFLUENCER': return '인플루언서';
  231. case 'VENDOR': return '벤더사';
  232. case 'BRAND': return '브랜드사';
  233. default: return '사용자';
  234. }
  235. };
  236. // 편집 모드 토글
  237. const toggleEditMode = () => {
  238. if (isEditMode.value) {
  239. // 편집 취소
  240. isEditMode.value = false;
  241. } else {
  242. // 편집 시작 - 현재 값으로 폼 초기화
  243. editForm.value = {
  244. name: myInfo.value.name || userInfo.value.name,
  245. nickName: myInfo.value.nickName || '',
  246. email: myInfo.value.email || userInfo.value.email,
  247. companyName: userInfo.value.companyName || '',
  248. phone: myInfo.value.phone || userInfo.value.phone,
  249. snsType: myInfo.value.sns_type || '',
  250. snsLink: myInfo.value.sns_link || '',
  251. newPassword: '',
  252. confirmPassword: ''
  253. };
  254. isEditMode.value = true;
  255. }
  256. };
  257. // 편집 취소
  258. const cancelEdit = () => {
  259. isEditMode.value = false;
  260. };
  261. // 프로필 저장
  262. const fnUpdate = async () => {
  263. // 패스워드 유효성 검사
  264. if (editForm.value.newPassword && editForm.value.newPassword !== editForm.value.confirmPassword) {
  265. $toast.error('패스워드가 일치하지 않습니다.');
  266. return;
  267. }
  268. if (editForm.value.newPassword && editForm.value.newPassword.length < 6) {
  269. $toast.error('패스워드는 최소 6자 이상이어야 합니다.');
  270. return;
  271. }
  272. let req = {
  273. MEMBER_TYPE: useAtStore.auth.memberType,
  274. MEMBER_SEQ: useAtStore.auth.seq,
  275. EMAIL: editForm.value.email,
  276. PHONE: editForm.value.phone,
  277. SNS_TYPE: editForm.value.snsType,
  278. SNS_LINK_ID: editForm.value.snsLink
  279. }
  280. // 패스워드가 입력된 경우에만 추가
  281. if (editForm.value.newPassword.trim()) {
  282. req.PASSWORD = editForm.value.newPassword;
  283. }
  284. // 인플루언서인 경우 닉네임 추가
  285. if (userInfo.value.memberType === 'INFLUENCER') {
  286. req.NICK_NAME = editForm.value.nickName;
  287. }
  288. useAxios().post('/mypage/update', req).then(async (res) => {
  289. // 로컬 데이터 업데이트
  290. myInfo.value.email = editForm.value.email;
  291. myInfo.value.phone = editForm.value.phone;
  292. myInfo.value.sns_type = editForm.value.snsType;
  293. myInfo.value.sns_link = editForm.value.snsLink;
  294. if (userInfo.value.memberType === 'INFLUENCER') {
  295. myInfo.value.nickName = editForm.value.nickName;
  296. }
  297. // 스토어도 업데이트
  298. useAtStore.auth.email = editForm.value.email;
  299. useAtStore.auth.phone = editForm.value.phone;
  300. $toast.success('프로필이 성공적으로 수정되었습니다.');
  301. isEditMode.value = false;
  302. })
  303. };
  304. const formatDate = (dateStr) => {
  305. if (!dateStr) return '';
  306. const date = new Date(dateStr);
  307. const year = date.getFullYear();
  308. const month = String(date.getMonth() + 1).padStart(2, '0');
  309. const day = String(date.getDate()).padStart(2, '0');
  310. return `${year}.${month}.${day}`;
  311. };
  312. // SNS 입력 placeholder 반환
  313. const getSnsPlaceholder = (snsType) => {
  314. const placeholders = {
  315. 'youtube': '채널 ID 또는 전체 URL',
  316. 'instagram': '사용자명 또는 전체 URL',
  317. 'naver': '블로그 ID 또는 전체 URL',
  318. 'tiktok': '사용자명 또는 전체 URL',
  319. 'facebook': '페이지명 또는 전체 URL',
  320. 'twitter': '사용자명 또는 전체 URL',
  321. 'blog': '블로그 주소',
  322. 'kakao': '사용자명 또는 전체 URL'
  323. };
  324. return placeholders[snsType] || 'SNS 링크 또는 ID를 입력하세요';
  325. };
  326. // 사용자 상세 정보 로드
  327. const fnDetail = () => {
  328. let req = {
  329. MEMBER_TYPE: useAtStore.auth.memberType,
  330. MEMBER_SEQ: useAtStore.auth.seq
  331. }
  332. useAxios()
  333. .post(`/mypage/detail`, req)
  334. .then((res) => {
  335. myInfo.value.id = res.data.ID;
  336. myInfo.value.name = res.data.NAME;
  337. myInfo.value.nickName = res.data.NICK_NAME;
  338. myInfo.value.phone = res.data.PHONE;
  339. myInfo.value.sns_type = res.data.SNS_TYPE;
  340. myInfo.value.sns_link = res.data.SNS_LINK_ID;
  341. myInfo.value.email = res.data.EMAIL;
  342. myInfo.value.regDate = res.data.REGDATE;
  343. })
  344. };
  345. /************************************************************************
  346. | 라이프사이클
  347. ************************************************************************/
  348. onMounted(() => {
  349. fnDetail();
  350. });
  351. </script>
  352. <style scoped>
  353. .mypage-container {
  354. margin: 0 auto;
  355. padding: 20px;
  356. }
  357. .page-header {
  358. margin-bottom: 32px;
  359. text-align: center;
  360. }
  361. .page-header h1 {
  362. font-size: 2.5rem;
  363. color: #1a237e;
  364. margin-bottom: 8px;
  365. }
  366. .page-header p {
  367. color: #666;
  368. font-size: 1.1rem;
  369. }
  370. .profile-section {
  371. margin-bottom: 32px;
  372. }
  373. .profile-card {
  374. background: white;
  375. border-radius: 20px;
  376. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  377. overflow: hidden;
  378. }
  379. .profile-header {
  380. display: flex;
  381. align-items: center;
  382. padding: 32px;
  383. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  384. color: white;
  385. }
  386. .profile-avatar {
  387. font-size: 3rem;
  388. margin-right: 24px;
  389. background: rgba(255, 255, 255, 0.2);
  390. width: 96px;
  391. height: 96px;
  392. overflow: hidden;
  393. border-radius: 50%;
  394. display: flex;
  395. align-items: center;
  396. justify-content: center;
  397. }
  398. .profile-info {
  399. flex: 1;
  400. }
  401. .profile-info h2 {
  402. margin-bottom: 15px;
  403. font-size: 1.8rem;
  404. }
  405. .member-type-badge {
  406. background: rgba(255, 255, 255, 0.2);
  407. padding: 4px 12px;
  408. border-radius: 20px;
  409. font-size: 0.9rem;
  410. }
  411. .edit-btn {
  412. background: rgba(255, 255, 255, 0.2);
  413. border: 2px solid rgba(255, 255, 255, 0.3);
  414. color: white;
  415. padding: 12px 24px;
  416. border-radius: 25px;
  417. cursor: pointer;
  418. transition: all 0.3s ease;
  419. font-weight: 500;
  420. }
  421. .edit-btn:hover, .edit-btn.active {
  422. background: white;
  423. color: #667eea;
  424. }
  425. .profile-content {
  426. padding: 32px;
  427. }
  428. /* 정보 표시 스타일 */
  429. .info-display .info-row {
  430. display: flex;
  431. padding: 16px 0;
  432. border-bottom: 1px solid #f0f0f0;
  433. }
  434. .info-display .info-row:last-child {
  435. border-bottom: none;
  436. }
  437. .info-display label {
  438. width: 150px;
  439. font-weight: 600;
  440. color: #333;
  441. }
  442. .info-display span {
  443. flex: 1;
  444. color: #666;
  445. }
  446. /* 편집 폼 스타일 */
  447. .info-edit .info-row {
  448. display: flex;
  449. padding: 16px 0;
  450. border-bottom: 1px solid #f0f0f0;
  451. align-items: center;
  452. }
  453. .info-edit .info-row.sns--row > .edit-input{
  454. width: 20%;
  455. flex: none;
  456. margin-right: 20px;
  457. }
  458. .info-edit .info-row.sns--row .edit-input-container{
  459. width: 100%;
  460. }
  461. .info-edit .info-row.sns--row .edit-input-container .edit-input{
  462. width: 100%;
  463. }
  464. .info-edit .info-row:last-child {
  465. border-bottom: none;
  466. }
  467. .info-edit label {
  468. width: 150px;
  469. font-weight: 600;
  470. color: #333;
  471. flex-shrink: 0;
  472. }
  473. .edit-input {
  474. width: 100%;
  475. padding: 12px 16px;
  476. border: 2px solid #e0e0e0;
  477. border-radius: 8px;
  478. font-size: 1rem;
  479. transition: border-color 0.3s ease;
  480. background: white;
  481. }
  482. .edit-input.w--20{
  483. width: 20%;
  484. }
  485. .edit-input:focus {
  486. outline: none;
  487. border-color: #667eea;
  488. }
  489. .edit-input:disabled {
  490. background: #f8f9fa;
  491. color: #6c757d;
  492. cursor: not-allowed;
  493. }
  494. .edit-help {
  495. display: block;
  496. margin-top: 4px;
  497. font-size: 0.8rem;
  498. color: #666;
  499. font-style: italic;
  500. }
  501. .edit-actions {
  502. display: flex;
  503. gap: 12px;
  504. margin-top: 32px;
  505. justify-content: flex-end;
  506. }
  507. .btn-save {
  508. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  509. color: white;
  510. border: none;
  511. padding: 12px 24px;
  512. border-radius: 8px;
  513. cursor: pointer;
  514. font-weight: 500;
  515. transition: all 0.3s ease;
  516. }
  517. .btn-save:hover:not(:disabled) {
  518. transform: translateY(-2px);
  519. box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
  520. }
  521. .btn-save:disabled {
  522. opacity: 0.6;
  523. cursor: not-allowed;
  524. }
  525. .btn-cancel {
  526. background: #f5f5f5;
  527. color: #666;
  528. border: none;
  529. padding: 12px 24px;
  530. border-radius: 8px;
  531. cursor: pointer;
  532. font-weight: 500;
  533. transition: all 0.3s ease;
  534. }
  535. .btn-cancel:hover {
  536. background: #e0e0e0;
  537. }
  538. /* 추가 정보 섹션 */
  539. .additional-info {
  540. margin-top: 32px;
  541. }
  542. .info-card {
  543. background: white;
  544. border-radius: 16px;
  545. padding: 32px;
  546. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
  547. }
  548. .info-card h3 {
  549. margin: 0 0 24px 0;
  550. color: #1a237e;
  551. font-size: 1.3rem;
  552. }
  553. .info-grid {
  554. display: grid;
  555. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  556. gap: 24px;
  557. }
  558. .info-item {
  559. padding: 16px;
  560. background: #f8f9fa;
  561. border-radius: 12px;
  562. }
  563. .info-label {
  564. display: block;
  565. font-size: 0.9rem;
  566. color: #666;
  567. margin-bottom: 15px;
  568. }
  569. .info-value {
  570. font-weight: 600;
  571. color: #333;
  572. font-size: 1rem;
  573. }
  574. </style>