feat(board-generator): add board code generator and sample CRUD artifacts

Add Node.js CLI tool with Handlebars templates for generating standard CRUD artifacts: Java entity, service, DAO, controller, MyBatis mapper XML, and Vue frontend pages.
Also generate the full SampleTableBoard CRUD reference implementation, update README with backend execution instructions, and add project plan documentation.
This commit is contained in:
2026-05-31 13:22:03 +09:00
parent d5ac812703
commit 12c40c6004
25 changed files with 3305 additions and 1 deletions
@@ -0,0 +1,219 @@
<template>
<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>{{menuName}}</strong> {{{detailModeExpression}}}</span>
<div class="float-end">
<button type="button" class="btn btn-secondary btn-sm" @click="moveToList()">목록</button>
</div>
</div>
<div class="ui--form-container">
{{#each formColumns}}
<div class="mb-3">
<label>{{label}}</label>
{{#if isTextarea}}
<textarea v-model.trim="form.{{propertyName}}" class="form-control" rows="6"></textarea>
{{else}}
<input v-model.trim="form.{{propertyName}}" type="text" class="form-control" />
{{/if}}
</div>
{{/each}}
<div class="ui--button-container">
<button type="button" class="btn btn-secondary btn-lg" @click="moveToList()">목록</button>
<button type="button" class="btn btn-primary btn-lg" @click="saveItem()">저장</button>
<button v-if="mode === 'EDIT'" type="button" class="btn btn-danger btn-lg" @click="deleteItem()">삭제</button>
</div>
</div>
</template>
<template v-else>
<div class="ui--list-heading clearfix">
<span class="ui--text-total"><strong>Total</strong> {{{totalCountExpression}}}</span>
<div class="float-end">
<button type="button" class="btn btn-primary btn-sm" @click="moveToCreate()">새 글</button>
</div>
</div>
<table class="table table-bordered ui--table">
<thead>
<tr>
{{#each listColumns}}
<th scope="col">{{label}}</th>
{{/each}}
</tr>
</thead>
<tbody>
<tr v-if="itemList.length === 0">
<td :colspan="{{listColumns.length}}" class="text-center">등록된 데이터가 없습니다.</td>
</tr>
<tr v-for="item in itemList" :key="item.{{primaryKeyProperty}}">
{{#each listColumns}}
<td class="text-center">
{{#if @first}}
<button type="button" class="btn btn-link ui--board-link" @click="openDetail(item.{{propertyName}})">{{{vueItemExpression}}}</button>
{{else}}
{{{vueItemExpression}}}
{{/if}}
</td>
{{/each}}
</tr>
</tbody>
</table>
</template>
</div>
</template>
<script>
import axios from 'axios';
import SDLUtil from '@/utils/SDLUtil';
export default {
name: '{{vueComponentName}}',
data() {
return {
itemList: [],
mode: 'CREATE',
form: this.createEmptyForm(),
};
},
computed: {
isDetailView() {
return Boolean(this.getRouteDetailId()) || this.$route.query.mode === 'create';
},
},
watch: {
'$route.query': {
handler() {
this.syncRouteState();
},
deep: true,
},
},
methods: {
createEmptyForm() {
return {
{{primaryKeyProperty}}: null,
{{#each formColumns}}
{{propertyName}}: '',
{{/each}}
};
},
init() {
this.fetchList();
this.syncRouteState();
},
getRouteDetailId() {
const detailId = this.$route.query.detailId;
return detailId || null;
},
syncRouteState() {
const detailId = this.getRouteDetailId();
if (detailId) {
this.fetchDetail(detailId);
return;
}
if (this.$route.query.mode === 'create') {
this.mode = 'CREATE';
this.form = this.createEmptyForm();
return;
}
this.mode = 'CREATE';
this.form = this.createEmptyForm();
},
async fetchList() {
SDLUtil.showLoadingBar(true);
try {
const { data } = await axios.post(`${SDLUtil.API_URL}{{requestBasePath}}list/main.do?method=search`, JSON.stringify({}), {
headers: { 'Content-Type': 'application/json' },
});
this.itemList = data.result || [];
} catch (error) {
SDLUtil.errorAlert(error);
} finally {
SDLUtil.showLoadingBar(false);
}
},
async fetchDetail(detailId) {
SDLUtil.showLoadingBar(true);
try {
const { data } = await axios.post(`${SDLUtil.API_URL}{{requestBasePath}}item/main.do?method=get{{entityName}}`, JSON.stringify({ {{primaryKeyProperty}}: detailId }), {
headers: { 'Content-Type': 'application/json' },
});
this.form = {
...this.createEmptyForm(),
...(data.result || {}),
};
this.mode = 'EDIT';
} catch (error) {
SDLUtil.errorAlert(error);
} finally {
SDLUtil.showLoadingBar(false);
}
},
moveToCreate() {
this.$router.push({ path: this.$route.path, query: { mode: 'create' } });
},
moveToList() {
this.$router.push({ path: this.$route.path, query: {} });
},
openDetail(detailId) {
this.$router.push({ path: this.$route.path, query: { detailId } });
},
async saveItem() {
SDLUtil.showLoadingBar(true);
try {
const requestUrl = this.mode === 'EDIT'
? `${SDLUtil.API_URL}{{requestBasePath}}item/main.do?method=update`
: `${SDLUtil.API_URL}{{requestBasePath}}item/main.do?method=regist`;
const { data } = await axios.post(requestUrl, JSON.stringify(this.form), {
headers: { 'Content-Type': 'application/json' },
});
await this.fetchList();
if (data.result && data.result.{{primaryKeyProperty}}) {
this.$router.replace({ path: this.$route.path, query: { detailId: data.result.{{primaryKeyProperty}} } });
}
SDLUtil.alert('저장되었습니다.');
} catch (error) {
SDLUtil.errorAlert(error);
} finally {
SDLUtil.showLoadingBar(false);
}
},
deleteItem() {
if (!this.form.{{primaryKeyProperty}}) {
return;
}
SDLUtil.confirm({
msg: '삭제하시겠습니까?',
onOkEvt: () => this.doDelete(),
});
},
async doDelete() {
SDLUtil.showLoadingBar(true);
try {
await axios.post(`${SDLUtil.API_URL}{{requestBasePath}}item/main.do?method=delete`, JSON.stringify({ {{primaryKeyProperty}}: this.form.{{primaryKeyProperty}} }), {
headers: { 'Content-Type': 'application/json' },
});
await this.fetchList();
this.moveToList();
SDLUtil.alert('삭제되었습니다.');
} catch (error) {
SDLUtil.errorAlert(error);
} finally {
SDLUtil.showLoadingBar(false);
}
},
},
mounted() {
this.$nextTick(() => {
this.init();
});
},
};
</script>
<style scoped>
.ui--board-link {
cursor: pointer;
padding: 0;
}
</style>