From f01f0026143ed1098d92bb2521687dc9e5ef4796 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 7 May 2026 14:16:26 +0900 Subject: [PATCH 1/7] =?UTF-8?q?refactor(account):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=83=81=ED=83=9C=20=ED=95=84=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20role=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 왜: userId/nickname/profileImage는 사용처가 없어 유지보수 단순화. 대신 권한 판별을 위해 role 필요.\n무엇: accountStore에서 세 필드 삭제, role 추가. isAuthenticated 동기화 수정. LOGIN/LOGOUT 로직 role 반영. Axios Authorization 유지. --- src/store/accountStore.js | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/store/accountStore.js b/src/store/accountStore.js index de53132..8929013 100644 --- a/src/store/accountStore.js +++ b/src/store/accountStore.js @@ -12,17 +12,13 @@ enhanceAccessToken(); const accountStore = { namespaced: true, state: { - userId: '', - nickname: '', accessToken: '', - profileImage: '', + role: '', }, getters: { isAuthenticated(state) { - state.userId = state.userId || localStorage.userId - state.nickname = state.nickname || localStorage.nickname - state.profileImage = state.profileImage || localStorage.profileImage state.accessToken = state.accessToken || localStorage.accessToken + state.role = state.role || localStorage.role return state.accessToken !== undefined && state.accessToken !== null && @@ -31,27 +27,19 @@ const accountStore = { } }, mutations: { - LOGIN(state, {userId, nickname, token, profileImage}) { - state.userId = userId - localStorage.userId = userId - - state.nickname = nickname - localStorage.nickname = nickname - - state.profileImage = profileImage - localStorage.profileImage = profileImage - + LOGIN(state, {token, role}) { state.accessToken = token localStorage.accessToken = token + state.role = role + localStorage.role = role + Vue.axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; }, LOGOUT(state) { - state.userId = '' - state.nickname = '' - state.profileImage = '' state.accessToken = '' + state.role = '' localStorage.clear() if (location.pathname === '/') { From 943533473424822385a309b1c9538850cca0c4d4 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 7 May 2026 14:20:40 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix(api):=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /member/login -> /admin/member/login - 프론트엔드 관리자 로그인 경로와 백엔드 변경사항 동기화 --- src/api/member.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/member.js b/src/api/member.js index 8796589..208f30f 100644 --- a/src/api/member.js +++ b/src/api/member.js @@ -1,7 +1,7 @@ import Vue from 'vue'; async function login(email, password) { - return Vue.axios.post('/member/login', { + return Vue.axios.post('/admin/member/login', { email, password, isAdmin: true, From c72e1c18df91ffc9d30ba17d4262c48d5af7047f Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 7 May 2026 14:28:03 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix(content):=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=EA=B3=BC=20=EB=B2=84=ED=8A=BC=EC=9D=84=20ADM?= =?UTF-8?q?IN=20=EA=B6=8C=ED=95=9C=EC=97=90=EB=A7=8C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배경: 비ADMIN 계정에서도 관리 열과 수정/삭제/공유 버튼이 노출되어 접근 혼란을 유발. 변경: computed isAdmin(Vuex accountStore.role 우선, localStorage 폴백) 추가 후, 테이블 헤더와 각 행의 관리 영역에 v-if="isAdmin" 적용. 영향: ADMIN 외 권한에서는 UI 요소가 렌더링되지 않음. 기능 동작 변경 없음. --- src/views/Content/ContentList.vue | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/views/Content/ContentList.vue b/src/views/Content/ContentList.vue index b759dec..e8eba9b 100644 --- a/src/views/Content/ContentList.vue +++ b/src/views/Content/ContentList.vue @@ -96,7 +96,10 @@ 오픈 예정일 - + 관리 @@ -214,7 +217,7 @@ {{ item.date }} {{ item.releaseDate }} - + Date: Thu, 7 May 2026 15:20:07 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat(menu):=20=EC=97=AD=ED=95=A0=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=82=AC=EC=9D=B4=EB=93=9C=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=B6=94=EA=B0=80/=EB=85=B8=EC=B6=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ADMIN 권한에만 추가 메뉴(시리즈 배너, 캐릭터 챗봇, 에이전트 관리, 정산 확장) 노출 - API 메뉴가 비어있고 CONTENT_MANAGER이면 '콘텐츠 리스트(/content/list)' 기본 메뉴 추가 - 기존 예외 처리 유지 --- src/components/SideMenu.vue | 210 ++++++++++++++++++++---------------- 1 file changed, 116 insertions(+), 94 deletions(-) diff --git a/src/components/SideMenu.vue b/src/components/SideMenu.vue index 072efa5..a4a5a27 100644 --- a/src/components/SideMenu.vue +++ b/src/components/SideMenu.vue @@ -94,113 +94,135 @@ export default { this.isLoading = true try { let res = await api.getMenus(); - if (res.status === 200 && res.data.success === true && res.data.data.length > 0) { - this.items = res.data.data + if (res.status === 200 && res.data.success === true) { + // 기본 메뉴 설정 (API 결과가 비어있을 수 있음) + this.items = Array.isArray(res.data.data) ? res.data.data : [] - // '시리즈 관리' 메뉴에 '배너 등록' 하위 메뉴 추가 - try { - const seriesMenu = this.items.find(m => m && m.title === '시리즈 관리') - if (seriesMenu) { - if (!Array.isArray(seriesMenu.items)) { - seriesMenu.items = seriesMenu.items ? [].concat(seriesMenu.items) : [] - } - const exists = seriesMenu.items.some(ci => ci && ci.route === '/content/series/banner') - if (!exists) { - seriesMenu.items.push({ - title: '배너 등록', - route: '/content/series/banner', - items: null - }) + // 현재 사용자 역할 확인 + const role = (this.$store && this.$store.state && this.$store.state.accountStore && this.$store.state.accountStore.role) + || localStorage.role + + // ADMIN 권한 전용 추가 메뉴들 + if (role === 'ADMIN') { + // '시리즈 관리' 메뉴에 '배너 등록' 하위 메뉴 추가 + try { + const seriesMenu = this.items.find(m => m && m.title === '시리즈 관리') + if (seriesMenu) { + if (!Array.isArray(seriesMenu.items)) { + seriesMenu.items = seriesMenu.items ? [].concat(seriesMenu.items) : [] + } + const exists = seriesMenu.items.some(ci => ci && ci.route === '/content/series/banner') + if (!exists) { + seriesMenu.items.push({ + title: '배너 등록', + route: '/content/series/banner', + items: null + }) + } } + } catch (e) { + // ignore } - } catch (e) { - // ignore - } - // 캐릭터 챗봇 메뉴 추가 - this.items.push({ - title: '캐릭터 챗봇', - route: null, - items: [ - { - title: '배너 등록', - route: '/character/banner', - items: null - }, - { - title: '캐릭터 리스트', - route: '/character', - items: null - }, - { - title: '큐레이션', - route: '/character/curation', - items: null - }, - { - title: '정산', - route: '/character/calculate', - items: null - }, - { - title: '원작', - route: '/original-work', - items: null - }, - ] - }) - - // 에이전트 관리 메뉴를 '크리에이터 관리' 바로 아래에 추가 - try { - const insertAfterTitle = '크리에이터 관리' - const agentMenu = { - title: '에이전트 관리', + // 캐릭터 챗봇 메뉴 추가 + this.items.push({ + title: '캐릭터 챗봇', route: null, items: [ - { title: '에이전트 리스트', route: '/agent/list', items: null }, - { title: '에이전트 정산 비율', route: '/agent/settlement-ratio', items: null }, + { + title: '배너 등록', + route: '/character/banner', + items: null + }, + { + title: '캐릭터 리스트', + route: '/character', + items: null + }, + { + title: '큐레이션', + route: '/character/curation', + items: null + }, + { + title: '정산', + route: '/character/calculate', + items: null + }, + { + title: '원작', + route: '/original-work', + items: null + }, ] + }) + + // 에이전트 관리 메뉴를 '크리에이터 관리' 바로 아래에 추가 + try { + const insertAfterTitle = '크리에이터 관리' + const agentMenu = { + title: '에이전트 관리', + route: null, + items: [ + { title: '에이전트 리스트', route: '/agent/list', items: null }, + { title: '에이전트 정산 비율', route: '/agent/settlement-ratio', items: null }, + ] + } + + const idx = this.items.findIndex(m => m && m.title === insertAfterTitle) + if (idx >= 0) { + this.items.splice(idx + 1, 0, agentMenu) + } else { + // 기준 메뉴가 없으면 하단에 추가 + this.items.push(agentMenu) + } + } catch (e) { + // ignore } - const idx = this.items.findIndex(m => m && m.title === insertAfterTitle) - if (idx >= 0) { - this.items.splice(idx + 1, 0, agentMenu) - } else { - // 기준 메뉴가 없으면 하단에 추가 - this.items.push(agentMenu) + // 정산현황 메뉴에 '채널 후원 정산' 및 '오리지널 시리즈 정산' 추가 + try { + const calculateMenu = this.items.find(m => m && m.title === '정산현황') + if (calculateMenu) { + if (!Array.isArray(calculateMenu.items)) { + calculateMenu.items = calculateMenu.items ? [].concat(calculateMenu.items) : [] + } + const exists = calculateMenu.items.some(ci => ci && ci.route === '/calculate/channel-donation') + if (!exists) { + calculateMenu.items.push({ + title: '채널 후원 정산', + route: '/calculate/channel-donation', + items: null + }) + } + + const existsOriginal = calculateMenu.items.some(ci => ci && ci.route === '/calculate/original-series') + if (!existsOriginal) { + calculateMenu.items.push({ + title: '오리지널 시리즈 정산', + route: '/calculate/original-series', + items: null + }) + } + } + } catch (e) { + // ignore } - } catch (e) { - // ignore } - // 정산현황 메뉴에 '채널 후원 정산' 추가 - try { - const calculateMenu = this.items.find(m => m && m.title === '정산현황') - if (calculateMenu) { - if (!Array.isArray(calculateMenu.items)) { - calculateMenu.items = calculateMenu.items ? [].concat(calculateMenu.items) : [] - } - const exists = calculateMenu.items.some(ci => ci && ci.route === '/calculate/channel-donation') - if (!exists) { - calculateMenu.items.push({ - title: '채널 후원 정산', - route: '/calculate/channel-donation', - items: null - }) - } + // 조회한 메뉴가 비어 있고, 콘텐츠 매니저라면 기본 메뉴 추가 + if (this.items.length === 0 && role === 'CONTENT_MANAGER') { + this.items.push({ + title: '콘텐츠 리스트', + route: '/content/list', + items: null + }) + } - // '오리지널 시리즈 정산' 추가 - const existsOriginal = calculateMenu.items.some(ci => ci && ci.route === '/calculate/original-series') - if (!existsOriginal) { - calculateMenu.items.push({ - title: '오리지널 시리즈 정산', - route: '/calculate/original-series', - items: null - }) - } - } - } catch (e) { - // ignore + // 그래도 비어있다면 이전 동작과 동일하게 처리 + if (this.items.length === 0) { + this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!") + this.logout(); } } else { this.notifyError("알 수 없는 오류가 발생했습니다. 다시 로그인 해주세요!") From e7c95ab91b0480a4ec5cde8fe98e380c69714208 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 7 May 2026 15:23:27 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix(content):=20ADMIN=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=A7=8C=20=ED=85=8C=EB=A7=88=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=ED=98=B8=EC=B6=9C=ED=95=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ContentList.vue의 created 훅에서 isAdmin 검사 후 getAudioContentThemeList 조건부 호출. 불필요한 API 호출 방지 및 권한 준수. --- src/views/Content/ContentList.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/views/Content/ContentList.vue b/src/views/Content/ContentList.vue index e8eba9b..b0e0a6f 100644 --- a/src/views/Content/ContentList.vue +++ b/src/views/Content/ContentList.vue @@ -550,7 +550,10 @@ export default { is_settlement_ratio_deleted: false, settlement_ratio: "", }; - await this.getAudioContentThemeList(); + // ADMIN 권한일 때만 테마 리스트 조회 + if (this.isAdmin) { + await this.getAudioContentThemeList(); + } await this.getAudioContent(); }, From a58a5cc0d192b988ecf1da45a01a9747d510dbdb Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 7 May 2026 15:27:53 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat(member):=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8C=9D=EC=97=85=EC=97=90=20=EC=BD=98?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=9D=BC=EB=B2=A8?= =?UTF-8?q?/=EA=B2=80=EC=83=89=20=EB=B2=84=ED=8A=BC=20=EC=83=89=EC=83=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 신규 권한: CONTENT_MANAGER 라디오 옵션 및 매핑 추가 - 라벨 변경: '사용 여부' → '권한' - 검색 버튼 색상: #3bb9f1로 변경 왜: 콘텐츠 관리자 권한 지원 및 UI 용어/가시성 개선 무엇: MemberList.vue 수정으로 옵션/매핑/라벨/컬러 반영 --- src/views/Member/MemberList.vue | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/views/Member/MemberList.vue b/src/views/Member/MemberList.vue index 8bcd212..cd3e198 100644 --- a/src/views/Member/MemberList.vue +++ b/src/views/Member/MemberList.vue @@ -19,7 +19,7 @@ > @@ -171,7 +171,7 @@ - 사용 여부 + 권한 + @@ -451,6 +455,8 @@ export default { this.user_type = 'CREATOR' } else if (member.userType === '에이전트') { this.user_type = 'AGENT' + } else if (member.userType === '콘텐츠 관리자') { + this.user_type = 'CONTENT_MANAGER' } this.email = member.email @@ -519,7 +525,8 @@ export default { if ( (this.user_type === 'CREATOR' && this.member.userType === '크리에이터') || (this.user_type === 'USER' && this.member.userType === '일반회원') || - (this.user_type === 'AGENT' && this.member.userType === '에이전트') + (this.user_type === 'AGENT' && this.member.userType === '에이전트') || + (this.user_type === 'CONTENT_MANAGER' && this.member.userType === '콘텐츠 관리자') ) { this.notifyError("변경사항이 없습니다.") } else { From 90377bdb3c1312888253bbb664b95eb5f5e6ab2b Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 7 May 2026 15:29:34 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat(content-list):=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=83=89=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검색 버튼 색상: #3bb9f1로 변경 --- src/views/Content/ContentList.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Content/ContentList.vue b/src/views/Content/ContentList.vue index b0e0a6f..0aa3d89 100644 --- a/src/views/Content/ContentList.vue +++ b/src/views/Content/ContentList.vue @@ -36,7 +36,7 @@ >