refactor: 프로젝트에서 공개 범위(visibility) 제거
불필요한 프로젝트 공개 범위 데이터를 백엔드·프론트에서 일괄 제거(저장소 Repo 의 visibility 는 Gitea 연동용이라 유지). 백엔드: - Project 엔티티 visibility 컬럼·ProjectVisibility 타입 제거(dev synchronize 가 컬럼 DROP) - create/update DTO, ProjectResponse, create/update/ensureUnassigned/toResponse 에서 제거 - ai.service 프로젝트 컨텍스트 문자열에서 공개범위 표기 제거 - activity 타입 project.visibility_changed 제거 프론트: - types/project·project.store·mock Project 에서 visibility 제거 - ProjectCreatePage·ProjectSettingsPage 공개 범위 입력 폼+CSS 제거 - ProjectListPage 공개여부 세그먼트 필터+배지 제거, ProjectHeader 배지 제거 - activity 타입/스토어 project.visibility_changed 케이스 제거 런타임 검증: DB projects.visibility 컬럼 DROP, 생성/목록 응답에 visibility 없음, CRUD 정상. 백엔드 build/lint 0·14 tests, 프론트 type-check/lint/build 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,40 +37,6 @@ const extra = computed(() => props.moreCount ?? props.repo.moreCount)
|
||||
<div class="rh-main">
|
||||
<div class="rh-title">
|
||||
<span class="rh-name">{{ repo.name }}</span>
|
||||
<span
|
||||
class="vis"
|
||||
:class="repo.visibility"
|
||||
>
|
||||
<svg
|
||||
v-if="repo.visibility === 'private'"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="11"
|
||||
width="18"
|
||||
height="11"
|
||||
rx="2"
|
||||
/><path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
/><path d="M2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20" />
|
||||
</svg>
|
||||
{{ repo.visibility === 'private' ? '비공개' : '공개' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rh-desc">
|
||||
{{ repo.desc }}
|
||||
|
||||
@@ -77,7 +77,6 @@ export interface Project {
|
||||
id: string
|
||||
name: string
|
||||
desc: string
|
||||
visibility: Visibility
|
||||
/** "미분류" 시스템 프로젝트 */
|
||||
isSystem: boolean
|
||||
members: User[]
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
// 새 프로젝트 만들기 — 표시 제목/설명/공개 범위
|
||||
// 새 프로젝트 만들기 — 표시 제목/설명
|
||||
import { ref } from 'vue'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import { useProjectStore } from '@/stores/project.store'
|
||||
import type { Visibility } from '@/mock/relay.mock'
|
||||
|
||||
const router = useRouter()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const name = ref('')
|
||||
const description = ref('')
|
||||
const visibility = ref<Visibility>('private')
|
||||
const submitting = ref(false)
|
||||
|
||||
// 생성 — 성공 시 새 프로젝트 상세로 이동
|
||||
@@ -26,7 +24,6 @@ async function createProject() {
|
||||
const project = await projectStore.create({
|
||||
name: name.value.trim(),
|
||||
description: description.value.trim() || undefined,
|
||||
visibility: visibility.value,
|
||||
})
|
||||
await router.push(`/projects/${project.id}`)
|
||||
} catch {
|
||||
@@ -79,39 +76,6 @@ async function createProject() {
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 공개 범위 -->
|
||||
<div class="fblock">
|
||||
<div class="flabel">
|
||||
<span class="req">*</span> 공개 범위
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<button
|
||||
class="radio-card"
|
||||
:class="{ sel: visibility === 'private' }"
|
||||
type="button"
|
||||
@click="visibility = 'private'"
|
||||
>
|
||||
<span class="rc-body">
|
||||
<span class="rc-title">비공개</span>
|
||||
<span class="rc-desc">초대된 멤버만 접근하고 업무를 확인할 수 있습니다.</span>
|
||||
</span>
|
||||
<span class="rc-radio" />
|
||||
</button>
|
||||
<button
|
||||
class="radio-card"
|
||||
:class="{ sel: visibility === 'public' }"
|
||||
type="button"
|
||||
@click="visibility = 'public'"
|
||||
>
|
||||
<span class="rc-body">
|
||||
<span class="rc-title">공개</span>
|
||||
<span class="rc-desc">조직 내 모든 구성원이 프로젝트를 보고 업무 현황을 열람할 수 있습니다.</span>
|
||||
</span>
|
||||
<span class="rc-radio" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 설명 -->
|
||||
<div class="fblock">
|
||||
<div class="flabel">
|
||||
@@ -234,68 +198,6 @@ textarea.tarea {
|
||||
resize: vertical;
|
||||
height: auto;
|
||||
}
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
.radio-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.8125rem;
|
||||
padding: 0.8125rem 0.875rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
.radio-card:hover {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
.radio-card.sel {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-weak);
|
||||
}
|
||||
.rc-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.rc-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.rc-desc {
|
||||
font-size: 0.781rem;
|
||||
color: var(--text-2);
|
||||
margin-top: 0.125rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.rc-radio {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid var(--border-strong);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #fff;
|
||||
}
|
||||
.radio-card.sel .rc-radio {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.radio-card.sel .rc-radio::after {
|
||||
content: '';
|
||||
width: 0.5625rem;
|
||||
height: 0.5625rem;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
}
|
||||
.form-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -6,12 +6,8 @@ import { RouterLink } from 'vue-router'
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { useProjectStore } from '@/stores/project.store'
|
||||
import type { Visibility } from '@/mock/relay.mock'
|
||||
|
||||
type Filter = 'all' | Visibility
|
||||
|
||||
const keyword = ref('')
|
||||
const filter = ref<Filter>('all')
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const { projects, loading, total, hasMore, loadingMore } = storeToRefs(projectStore)
|
||||
@@ -20,15 +16,11 @@ onMounted(() => {
|
||||
void projectStore.fetchList()
|
||||
})
|
||||
|
||||
// 검색어 + 공개여부 필터 적용
|
||||
// 검색어 필터 적용
|
||||
const filteredRepos = computed(() =>
|
||||
projects.value.filter((project) => {
|
||||
const matchKeyword =
|
||||
!keyword.value || project.name.includes(keyword.value)
|
||||
const matchFilter =
|
||||
filter.value === 'all' || project.visibility === filter.value
|
||||
return matchKeyword && matchFilter
|
||||
}),
|
||||
projects.value.filter(
|
||||
(project) => !keyword.value || project.name.includes(keyword.value),
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -69,26 +61,6 @@ const filteredRepos = computed(() =>
|
||||
placeholder="프로젝트 검색…"
|
||||
>
|
||||
</div>
|
||||
<div class="segmented">
|
||||
<button
|
||||
:class="{ active: filter === 'all' }"
|
||||
@click="filter = 'all'"
|
||||
>
|
||||
전체
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: filter === 'private' }"
|
||||
@click="filter = 'private'"
|
||||
>
|
||||
비공개
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: filter === 'public' }"
|
||||
@click="filter = 'public'"
|
||||
>
|
||||
공개
|
||||
</button>
|
||||
</div>
|
||||
<div class="sort">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
@@ -151,42 +123,6 @@ const filteredRepos = computed(() =>
|
||||
v-if="repo.isSystem"
|
||||
class="repo-slug"
|
||||
>시스템</span>
|
||||
<span
|
||||
class="vis"
|
||||
:class="repo.visibility"
|
||||
>
|
||||
<svg
|
||||
v-if="repo.visibility === 'private'"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="11"
|
||||
width="18"
|
||||
height="11"
|
||||
rx="2"
|
||||
/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
/>
|
||||
<path d="M2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20" />
|
||||
</svg>
|
||||
{{ repo.visibility === 'private' ? '비공개' : '공개' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="repo-desc">
|
||||
{{ repo.desc }}
|
||||
|
||||
@@ -6,7 +6,7 @@ import AppShell from '@/layouts/AppShell.vue'
|
||||
import ProjectHeader from '@/components/ProjectHeader.vue'
|
||||
import ProjectTabs from '@/components/ProjectTabs.vue'
|
||||
import { useProjectStore } from '@/stores/project.store'
|
||||
import { type Project, type Visibility } from '@/mock/relay.mock'
|
||||
import { type Project } from '@/mock/relay.mock'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -26,7 +26,6 @@ const memberCount = computed(() =>
|
||||
// 폼 상태(초기값은 프로젝트 데이터)
|
||||
const displayTitle = ref('')
|
||||
const description = ref('')
|
||||
const visibility = ref<Visibility>('private')
|
||||
const saving = ref(false)
|
||||
|
||||
// 프로젝트 로드 + 폼 초기화
|
||||
@@ -45,7 +44,6 @@ async function loadRepo() {
|
||||
}
|
||||
displayTitle.value = repo.value.name
|
||||
description.value = repo.value.desc
|
||||
visibility.value = repo.value.visibility
|
||||
}
|
||||
watch(projectId, loadRepo, { immediate: true })
|
||||
|
||||
@@ -57,7 +55,6 @@ async function save() {
|
||||
repo.value = await projectStore.update(projectId.value, {
|
||||
name: displayTitle.value.trim(),
|
||||
description: description.value.trim(),
|
||||
visibility: visibility.value,
|
||||
})
|
||||
window.alert('변경 사항을 저장했습니다.')
|
||||
} catch {
|
||||
@@ -170,67 +167,6 @@ async function removeRepo() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="fblock">
|
||||
<div class="flabel">
|
||||
공개 범위
|
||||
</div>
|
||||
<div class="fhint">
|
||||
프로젝트와 그 안의 업무를 누가 볼 수 있는지 결정합니다.
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<button
|
||||
class="radio-card"
|
||||
:class="{ sel: visibility === 'private' }"
|
||||
type="button"
|
||||
@click="visibility = 'private'"
|
||||
>
|
||||
<span class="rc-ico">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
><rect
|
||||
x="3"
|
||||
y="11"
|
||||
width="18"
|
||||
height="11"
|
||||
rx="2"
|
||||
/><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>
|
||||
</span>
|
||||
<span class="rc-body">
|
||||
<span class="rc-title">비공개</span>
|
||||
<span class="rc-desc">프로젝트에 초대된 멤버만 접근하고 업무를 확인할 수 있습니다.</span>
|
||||
</span>
|
||||
<span class="rc-radio" />
|
||||
</button>
|
||||
<button
|
||||
class="radio-card"
|
||||
:class="{ sel: visibility === 'public' }"
|
||||
type="button"
|
||||
@click="visibility = 'public'"
|
||||
>
|
||||
<span class="rc-ico">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
><circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
/><path d="M2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20" /></svg>
|
||||
</span>
|
||||
<span class="rc-body">
|
||||
<span class="rc-title">공개</span>
|
||||
<span class="rc-desc">조직 내 모든 구성원이 프로젝트를 보고 업무 현황을 열람할 수 있습니다.</span>
|
||||
</span>
|
||||
<span class="rc-radio" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-foot">
|
||||
<button
|
||||
class="btn primary"
|
||||
@@ -421,88 +357,6 @@ textarea.tarea {
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
/* 공개 범위 라디오 카드 */
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
.radio-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.8125rem;
|
||||
padding: 0.8125rem 0.875rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
.radio-card:hover {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
.radio-card.sel {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-weak);
|
||||
}
|
||||
.rc-ico {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: #f1f2f4;
|
||||
color: var(--text-2);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rc-ico svg {
|
||||
width: 1.0625rem;
|
||||
height: 1.0625rem;
|
||||
}
|
||||
.radio-card.sel .rc-ico {
|
||||
background: #fff;
|
||||
color: var(--accent);
|
||||
}
|
||||
.rc-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.rc-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.rc-desc {
|
||||
font-size: 0.781rem;
|
||||
color: var(--text-2);
|
||||
margin-top: 0.125rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.rc-radio {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid var(--border-strong);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.4375rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #fff;
|
||||
}
|
||||
.radio-card.sel .rc-radio {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.radio-card.sel .rc-radio::after {
|
||||
content: '';
|
||||
width: 0.5625rem;
|
||||
height: 0.5625rem;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.input-ico {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -45,13 +45,6 @@ function toRepoActivityItem(api: ApiActivity): RepoActivityItem {
|
||||
icon: 'pencil',
|
||||
html: `<b>${actor}</b>님이 표시 제목을 ‘${escapeHtml(pstr(p, 'newName'))}’(으)로 변경했습니다`,
|
||||
}
|
||||
case 'project.visibility_changed':
|
||||
return {
|
||||
...base,
|
||||
tone: 'setting',
|
||||
icon: 'lock',
|
||||
html: `<b>${actor}</b>님이 프로젝트 공개 범위를 <b>${pstr(p, 'visibility') === 'private' ? '비공개' : '공개'}</b>(으)로 변경했습니다`,
|
||||
}
|
||||
case 'repo.linked':
|
||||
return { ...base, tone: 'setting', icon: 'repo', html: `<b>${actor}</b>님이 저장소 <b>${escapeHtml(pstr(p, 'repoName'))}</b>을(를) 연동했습니다` }
|
||||
case 'repo.unlinked':
|
||||
|
||||
@@ -31,7 +31,6 @@ function toProjectView(api: ApiProject): ProjectView {
|
||||
id: api.id,
|
||||
name: api.name,
|
||||
desc: api.description ?? '',
|
||||
visibility: api.visibility,
|
||||
isSystem: api.isSystem,
|
||||
members: shown,
|
||||
moreCount: Math.max(0, api.memberCount - shown.length),
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { ApiMember } from '@/types/repo'
|
||||
export type ApiActivityType =
|
||||
| 'project.created'
|
||||
| 'project.renamed'
|
||||
| 'project.visibility_changed'
|
||||
| 'repo.linked'
|
||||
| 'repo.unlinked'
|
||||
| 'member.invited'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 프로젝트 도메인 타입 — 백엔드 ProjectService 응답 / DTO 와 1:1
|
||||
|
||||
import type { Visibility } from '@/mock/relay.mock'
|
||||
import type { ApiMember } from '@/types/repo'
|
||||
|
||||
// 프로젝트(API 원시) — 백엔드 ProjectResponse 와 1:1
|
||||
@@ -8,7 +7,6 @@ export interface ApiProject {
|
||||
id: string // UUID (라우팅 식별자)
|
||||
name: string
|
||||
description: string | null
|
||||
visibility: Visibility
|
||||
isSystem: boolean // "미분류" 시스템 프로젝트
|
||||
members: ApiMember[]
|
||||
memberCount: number
|
||||
@@ -25,12 +23,10 @@ export interface ApiProject {
|
||||
export interface CreateProjectPayload {
|
||||
name: string
|
||||
description?: string
|
||||
visibility?: Visibility
|
||||
}
|
||||
|
||||
// 프로젝트 수정 요청
|
||||
export interface UpdateProjectPayload {
|
||||
name?: string
|
||||
description?: string
|
||||
visibility?: Visibility
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user