feat(admin/sampleBoard): add route-based list/detail views
Restructured the sample board admin page to support separate list, create, edit, and detail views synced with URL query parameters. Added computed state, route watchers, and navigation methods to handle view switching, updated form layout to full width for detail displays, replaced inline title links with accessible styled buttons, and improved save/delete workflows to maintain correct component and URL state.
This commit is contained in:
@@ -4,74 +4,19 @@
|
||||
<div class="ui--content-wrapper">
|
||||
<!-- 공통 브레드크럼을 출력한다. -->
|
||||
<sdl-breadcrumb></sdl-breadcrumb>
|
||||
<!-- 목록 헤더 영역이다. -->
|
||||
<!-- 상세 화면 여부에 따라 목록 또는 상세를 출력한다. -->
|
||||
<template v-if="isDetailView">
|
||||
<!-- 상세 헤더 영역이다. -->
|
||||
<div class="ui--list-heading clearfix">
|
||||
<!-- 전체 건수를 출력한다. -->
|
||||
<span class="ui--text-total"><strong>Total</strong> {{ $filters.numberFormat(boardList.length) }}</span>
|
||||
<!-- 상세 화면 제목을 출력한다. -->
|
||||
<span class="ui--text-total"><strong>Sample Board</strong> {{ mode === 'CREATE' ? '등록' : '상세' }}</span>
|
||||
<!-- 우측 버튼 영역이다. -->
|
||||
<div class="float-end">
|
||||
<!-- 신규 등록 폼으로 초기화한다. -->
|
||||
<button type="button" class="btn btn-primary btn-sm" @click="resetForm()">새 글</button>
|
||||
<!-- 목록 화면으로 이동한다. -->
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="moveToList()">목록</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 목록과 입력 폼을 2단으로 배치한다. -->
|
||||
<div class="row">
|
||||
<!-- 좌측 목록 영역이다. -->
|
||||
<div class="col-6">
|
||||
<!-- 게시글 목록 테이블이다. -->
|
||||
<table class="table table-bordered ui--table">
|
||||
<!-- 컬럼 폭 정의이다. -->
|
||||
<colgroup>
|
||||
<!-- 번호 컬럼 폭이다. -->
|
||||
<col style="width: 12%" />
|
||||
<!-- 제목 컬럼 폭이다. -->
|
||||
<col style="width: 40%" />
|
||||
<!-- 작성자 컬럼 폭이다. -->
|
||||
<col style="width: 20%" />
|
||||
<!-- 등록일 컬럼 폭이다. -->
|
||||
<col style="width: 28%" />
|
||||
</colgroup>
|
||||
<!-- 테이블 헤더이다. -->
|
||||
<thead>
|
||||
<!-- 헤더 행이다. -->
|
||||
<tr>
|
||||
<!-- 번호 헤더이다. -->
|
||||
<th scope="col">번호</th>
|
||||
<!-- 제목 헤더이다. -->
|
||||
<th scope="col">제목</th>
|
||||
<!-- 작성자 헤더이다. -->
|
||||
<th scope="col">작성자</th>
|
||||
<!-- 등록일 헤더이다. -->
|
||||
<th scope="col">등록일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- 테이블 본문이다. -->
|
||||
<tbody>
|
||||
<!-- 데이터가 없을 때 안내 행을 출력한다. -->
|
||||
<tr v-if="boardList.length === 0">
|
||||
<!-- 빈 목록 안내 셀이다. -->
|
||||
<td colspan="4" class="text-center">등록된 게시글이 없습니다.</td>
|
||||
</tr>
|
||||
<!-- 게시글 목록을 반복 출력한다. -->
|
||||
<tr v-for="board in boardList" :key="board.id" :class="{ 'table-primary': board.id === form.id }">
|
||||
<!-- 게시글 번호를 출력한다. -->
|
||||
<td class="text-center">{{ board.id }}</td>
|
||||
<!-- 제목을 클릭하면 상세를 조회한다. -->
|
||||
<td>
|
||||
<!-- 상세 조회 버튼처럼 동작하는 링크이다. -->
|
||||
<a href="javascript:void(0);" @click="selectBoard(board.id)">{{ board.title }}</a>
|
||||
</td>
|
||||
<!-- 작성자를 출력한다. -->
|
||||
<td class="text-center">{{ board.author }}</td>
|
||||
<!-- 등록일을 출력한다. -->
|
||||
<td class="text-center">{{ formatDate(board.createdAt) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- 우측 입력 폼 영역이다. -->
|
||||
<div class="col-6">
|
||||
<!-- 입력 폼 컨테이너이다. -->
|
||||
<!-- 상세 입력 폼 컨테이너이다. -->
|
||||
<div class="ui--form-container">
|
||||
<!-- 폼 제목을 출력한다. -->
|
||||
<h4 class="mb-3">Sample Board {{ mode === 'CREATE' ? '등록' : '수정' }}</h4>
|
||||
@@ -115,16 +60,81 @@
|
||||
</div>
|
||||
<!-- 버튼 영역이다. -->
|
||||
<div class="ui--button-container">
|
||||
<!-- 입력 폼을 초기화한다. -->
|
||||
<button type="button" class="btn btn-secondary btn-lg" @click="resetForm()">초기화</button>
|
||||
<!-- 목록 화면으로 이동한다. -->
|
||||
<button type="button" class="btn btn-secondary btn-lg" @click="moveToList()">목록</button>
|
||||
<!-- 현재 입력값을 저장한다. -->
|
||||
<button type="button" class="btn btn-primary btn-lg" @click="saveBoard()">저장</button>
|
||||
<!-- 수정 모드일 때만 삭제 버튼을 노출한다. -->
|
||||
<button v-if="mode === 'EDIT'" type="button" class="btn btn-danger btn-lg" @click="confirmDelete(form.id)">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 목록 화면일 때만 게시글 목록을 출력한다. -->
|
||||
<template v-else>
|
||||
<!-- 목록 헤더 영역이다. -->
|
||||
<div class="ui--list-heading clearfix">
|
||||
<!-- 전체 건수를 출력한다. -->
|
||||
<span class="ui--text-total"><strong>Total</strong> {{ $filters.numberFormat(boardList.length) }}</span>
|
||||
<!-- 우측 버튼 영역이다. -->
|
||||
<div class="float-end">
|
||||
<!-- 신규 등록 상세 화면으로 이동한다. -->
|
||||
<button type="button" class="btn btn-primary btn-sm" @click="moveToCreate()">새 글</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 목록 테이블 영역이다. -->
|
||||
<div>
|
||||
<!-- 게시글 목록 테이블이다. -->
|
||||
<table class="table table-bordered ui--table">
|
||||
<!-- 컬럼 폭 정의이다. -->
|
||||
<colgroup>
|
||||
<!-- 번호 컬럼 폭이다. -->
|
||||
<col style="width: 12%" />
|
||||
<!-- 제목 컬럼 폭이다. -->
|
||||
<col style="width: 40%" />
|
||||
<!-- 작성자 컬럼 폭이다. -->
|
||||
<col style="width: 20%" />
|
||||
<!-- 등록일 컬럼 폭이다. -->
|
||||
<col style="width: 28%" />
|
||||
</colgroup>
|
||||
<!-- 테이블 헤더이다. -->
|
||||
<thead>
|
||||
<!-- 헤더 행이다. -->
|
||||
<tr>
|
||||
<!-- 번호 헤더이다. -->
|
||||
<th scope="col">번호</th>
|
||||
<!-- 제목 헤더이다. -->
|
||||
<th scope="col">제목</th>
|
||||
<!-- 작성자 헤더이다. -->
|
||||
<th scope="col">작성자</th>
|
||||
<!-- 등록일 헤더이다. -->
|
||||
<th scope="col">등록일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- 테이블 본문이다. -->
|
||||
<tbody>
|
||||
<!-- 데이터가 없을 때 안내 행을 출력한다. -->
|
||||
<tr v-if="boardList.length === 0">
|
||||
<!-- 빈 목록 안내 셀이다. -->
|
||||
<td colspan="4" class="text-center">등록된 게시글이 없습니다.</td>
|
||||
</tr>
|
||||
<!-- 게시글 목록을 반복 출력한다. -->
|
||||
<tr v-for="board in boardList" :key="board.id">
|
||||
<!-- 게시글 번호를 출력한다. -->
|
||||
<td class="text-center">{{ board.id }}</td>
|
||||
<!-- 제목을 클릭하면 상세 화면으로 이동한다. -->
|
||||
<td>
|
||||
<!-- 상세 화면 이동 버튼이다. -->
|
||||
<button type="button" class="btn btn-link ui--board-link" @click="openBoardDetail(board.id)">{{ board.title }}</button>
|
||||
</td>
|
||||
<!-- 작성자를 출력한다. -->
|
||||
<td class="text-center">{{ board.author }}</td>
|
||||
<!-- 등록일을 출력한다. -->
|
||||
<td class="text-center">{{ formatDate(board.createdAt) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -157,6 +167,29 @@ export default {
|
||||
},
|
||||
};
|
||||
},
|
||||
// 화면 파생 상태를 계산한다.
|
||||
computed: {
|
||||
// 현재 상세 화면 여부를 반환한다.
|
||||
isDetailView() {
|
||||
// 상세 조회 또는 등록 요청 여부를 반환한다.
|
||||
return Boolean(this.getRouteDetailId()) || this.$route.query.mode === 'create';
|
||||
},
|
||||
},
|
||||
// 라우트 쿼리 변경을 감시한다.
|
||||
watch: {
|
||||
// 상세 화면 전환을 처리한다.
|
||||
'$route.query': {
|
||||
// 쿼리가 바뀔 때마다 화면 상태를 동기화한다.
|
||||
handler() {
|
||||
// 현재 라우트 상태에 맞춰 화면을 전환한다.
|
||||
this.syncRouteState();
|
||||
},
|
||||
// 컴포넌트 초기 진입 시에도 즉시 실행한다.
|
||||
immediate: false,
|
||||
// 중첩된 쿼리 변경도 감시한다.
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
// 화면 동작 메서드를 정의한다.
|
||||
methods: {
|
||||
// 빈 입력 폼 객체를 생성한다.
|
||||
@@ -179,6 +212,41 @@ export default {
|
||||
init() {
|
||||
// 목록을 먼저 조회한다.
|
||||
this.fetchBoardList();
|
||||
// 현재 라우트 상태를 화면에 반영한다.
|
||||
this.syncRouteState();
|
||||
},
|
||||
// 라우트 쿼리에서 상세 식별자를 추출한다.
|
||||
getRouteDetailId() {
|
||||
// 상세 식별자 원본 값을 읽는다.
|
||||
const detailId = Number(this.$route.query.detailId);
|
||||
// 상세 식별자가 없거나 숫자가 아니면 null을 반환한다.
|
||||
if (!this.$route.query.detailId || Number.isNaN(detailId)) {
|
||||
// 유효한 상세 식별자가 없음을 반환한다.
|
||||
return null;
|
||||
}
|
||||
// 숫자로 변환된 상세 식별자를 반환한다.
|
||||
return detailId;
|
||||
},
|
||||
// 현재 라우트 상태에 맞는 화면을 구성한다.
|
||||
syncRouteState() {
|
||||
// 라우트에서 상세 식별자를 읽는다.
|
||||
const detailId = this.getRouteDetailId();
|
||||
// 상세 식별자가 있으면 상세 데이터를 조회한다.
|
||||
if (detailId) {
|
||||
// 상세 조회를 수행한다.
|
||||
this.selectBoard(detailId);
|
||||
// 상세 화면 처리 후 메서드를 종료한다.
|
||||
return;
|
||||
}
|
||||
// 등록 화면 요청이면 빈 폼으로 초기화한다.
|
||||
if (this.$route.query.mode === 'create') {
|
||||
// 등록용 폼으로 초기화한다.
|
||||
this.resetForm();
|
||||
// 등록 화면 처리 후 메서드를 종료한다.
|
||||
return;
|
||||
}
|
||||
// 목록 화면이면 폼 상태를 기본값으로 되돌린다.
|
||||
this.resetForm();
|
||||
},
|
||||
// 게시글 목록을 조회한다.
|
||||
async fetchBoardList() {
|
||||
@@ -199,6 +267,11 @@ export default {
|
||||
},
|
||||
// 게시글 상세를 조회한다.
|
||||
async selectBoard(id) {
|
||||
// 식별자가 없으면 함수 실행을 종료한다.
|
||||
if (!id) {
|
||||
// 함수 실행을 종료한다.
|
||||
return;
|
||||
}
|
||||
// 로딩 바를 표시한다.
|
||||
SDLUtil.showLoadingBar(true);
|
||||
try {
|
||||
@@ -227,6 +300,45 @@ export default {
|
||||
SDLUtil.showLoadingBar(false);
|
||||
}
|
||||
},
|
||||
// 상세 화면으로 이동한다.
|
||||
openBoardDetail(id) {
|
||||
// 이동할 식별자가 없으면 종료한다.
|
||||
if (!id) {
|
||||
// 함수 실행을 종료한다.
|
||||
return;
|
||||
}
|
||||
// 이미 같은 상세 화면이면 중복 이동을 생략한다.
|
||||
if (this.getRouteDetailId() === id) {
|
||||
// 함수 실행을 종료한다.
|
||||
return;
|
||||
}
|
||||
// 상세 화면 쿼리로 이동한다.
|
||||
this.$router.push({ path: this.$route.path, query: { detailId: id } });
|
||||
},
|
||||
// 신규 등록 상세 화면으로 이동한다.
|
||||
moveToCreate() {
|
||||
// 이미 등록 화면이면 중복 이동을 생략한다.
|
||||
if (this.$route.query.mode === 'create') {
|
||||
// 폼만 다시 초기화한다.
|
||||
this.resetForm();
|
||||
// 함수 실행을 종료한다.
|
||||
return;
|
||||
}
|
||||
// 등록 화면 쿼리로 이동한다.
|
||||
this.$router.push({ path: this.$route.path, query: { mode: 'create' } });
|
||||
},
|
||||
// 목록 화면으로 이동한다.
|
||||
moveToList() {
|
||||
// 이미 목록 화면이면 폼만 초기화한다.
|
||||
if (Object.keys(this.$route.query).length === 0) {
|
||||
// 폼 상태를 초기화한다.
|
||||
this.resetForm();
|
||||
// 함수 실행을 종료한다.
|
||||
return;
|
||||
}
|
||||
// 목록 화면 쿼리로 이동한다.
|
||||
this.$router.push({ path: this.$route.path, query: {} });
|
||||
},
|
||||
// 입력 폼을 신규 등록 상태로 초기화한다.
|
||||
resetForm() {
|
||||
// 모드를 등록으로 변경한다.
|
||||
@@ -295,10 +407,25 @@ export default {
|
||||
}
|
||||
// 저장 후 목록을 새로 조회한다.
|
||||
await this.fetchBoardList();
|
||||
// 저장된 게시글이 있으면 상세를 다시 로드한다.
|
||||
// 저장된 게시글이 있으면 상세 화면 상태를 유지한다.
|
||||
if (savedData && savedData.id) {
|
||||
// 저장된 게시글 상세를 선택한다.
|
||||
await this.selectBoard(savedData.id);
|
||||
// 저장된 결과를 현재 폼에 반영한다.
|
||||
this.form = {
|
||||
// 게시글 번호를 반영한다.
|
||||
id: savedData.id,
|
||||
// 제목을 반영한다.
|
||||
title: savedData.title,
|
||||
// 내용을 반영한다.
|
||||
content: savedData.content,
|
||||
// 작성자를 반영한다.
|
||||
author: savedData.author,
|
||||
// 등록일을 반영한다.
|
||||
createdAt: savedData.createdAt,
|
||||
};
|
||||
// 수정 모드로 전환한다.
|
||||
this.mode = 'EDIT';
|
||||
// 저장된 게시글 상세 화면으로 주소를 동기화한다.
|
||||
this.$router.replace({ path: this.$route.path, query: { detailId: savedData.id } });
|
||||
}
|
||||
// 저장 완료 알림을 출력한다.
|
||||
SDLUtil.alert('저장되었습니다.');
|
||||
@@ -337,8 +464,8 @@ export default {
|
||||
await axios.delete(`${SDLUtil.API_URL}/pg-board/samples/${id}`);
|
||||
// 삭제 후 목록을 새로 조회한다.
|
||||
await this.fetchBoardList();
|
||||
// 삭제 후 폼을 초기화한다.
|
||||
this.resetForm();
|
||||
// 삭제 후 목록 화면으로 이동한다.
|
||||
this.moveToList();
|
||||
// 삭제 완료 알림을 출력한다.
|
||||
SDLUtil.alert('삭제되었습니다.');
|
||||
} catch (err) {
|
||||
@@ -373,9 +500,11 @@ export default {
|
||||
|
||||
<!-- 샘플 게시판 CRUD 페이지 스타일이다. -->
|
||||
<style scoped>
|
||||
/* 제목 링크 커서를 포인터로 보이게 한다. */
|
||||
a {
|
||||
/* 링크 클릭 가능 상태를 표현한다. */
|
||||
/* 제목 버튼을 링크처럼 보이게 한다. */
|
||||
.ui--board-link {
|
||||
/* 버튼 클릭 가능 상태를 표현한다. */
|
||||
cursor: pointer;
|
||||
/* 버튼 좌우 패딩을 제거한다. */
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user