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:
@@ -0,0 +1,98 @@
|
||||
package {{controllerPackage}};
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
{{#each controllerImports}}
|
||||
import {{this}};
|
||||
{{/each}}
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import {{rootPackage}}.{{serviceName}};
|
||||
import {{entityFqcn}};
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("{{requestBasePath}}")
|
||||
@Log4j2
|
||||
public class {{controllerName}} {
|
||||
|
||||
private final {{serviceName}} {{entityVarName}}Service;
|
||||
|
||||
public {{controllerName}}({{serviceName}} {{entityVarName}}Service) {
|
||||
|
||||
this.{{entityVarName}}Service = {{entityVarName}}Service;
|
||||
}
|
||||
|
||||
@Operation(summary = "page 초기정보.")
|
||||
@PostMapping(value = "list/main.do", params = "method=page")
|
||||
public Map<String, Object> page(@RequestBody Map<String, Object> params) throws Exception {
|
||||
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
result.put("menuName", "{{menuName}}");
|
||||
result.put("result", {{entityVarName}}Service.get{{entityName}}List());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Operation(summary = "{{menuName}} 목록 조회")
|
||||
@PostMapping(value = "list/main.do", params = "method=search")
|
||||
public Map<String, Object> list_search(@RequestBody Map<String, Object> params) throws Exception {
|
||||
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
result.put("result", {{entityVarName}}Service.get{{entityName}}List());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Operation(summary = "{{menuName}} 상세 조회")
|
||||
@PostMapping(value = "item/main.do", params = "method=get{{entityName}}")
|
||||
public Map<String, Object> item_get{{entityName}}(@RequestBody Map<String, Object> params) throws Exception {
|
||||
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
result.put("result", {{entityVarName}}Service.get{{entityName}}(getPrimaryKey(params)));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Operation(summary = "{{menuName}} 등록")
|
||||
@PostMapping(value = "item/main.do", params = "method=regist")
|
||||
public Map<String, Object> item_regist(@RequestBody {{entityName}} {{entityVarName}}) throws Exception {
|
||||
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
result.put("result", {{entityVarName}}Service.create{{entityName}}({{entityVarName}}));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Operation(summary = "{{menuName}} 수정")
|
||||
@PostMapping(value = "item/main.do", params = "method=update")
|
||||
public Map<String, Object> item_update(@RequestBody {{entityName}} {{entityVarName}}) throws Exception {
|
||||
|
||||
if ({{entityVarName}}.{{primaryKeyGetterName}}() == null) {
|
||||
throw new IllegalArgumentException("{{primaryKeyProperty}} 값은 필수입니다.");
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
result.put("result", {{entityVarName}}Service.update{{entityName}}({{entityVarName}}));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Operation(summary = "{{menuName}} 삭제")
|
||||
@PostMapping(value = "item/main.do", params = "method=delete")
|
||||
public String item_delete(@RequestBody Map<String, Object> params) throws Exception {
|
||||
|
||||
{{entityVarName}}Service.delete{{entityName}}(getPrimaryKey(params));
|
||||
return "success";
|
||||
}
|
||||
|
||||
private {{primaryKeyJavaType}} getPrimaryKey(Map<String, Object> params) {
|
||||
|
||||
if (params == null || params.get("{{primaryKeyProperty}}") == null) {
|
||||
throw new IllegalArgumentException("{{primaryKeyProperty}} 값은 필수입니다.");
|
||||
}
|
||||
|
||||
String value = params.get("{{primaryKeyProperty}}").toString();
|
||||
return {{primaryKeyParseExpression}};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package {{daoPackage}};
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import {{entityFqcn}};
|
||||
|
||||
@Repository
|
||||
public class {{daoName}} {
|
||||
|
||||
private static final String MAPPER = "{{mapperNamespace}}.";
|
||||
|
||||
private final SqlSession sqlSession;
|
||||
|
||||
public {{daoName}}(SqlSession sqlSession) {
|
||||
|
||||
this.sqlSession = sqlSession;
|
||||
}
|
||||
|
||||
public List<{{entityName}}> select{{entityName}}List() {
|
||||
|
||||
return sqlSession.selectList(MAPPER + "select{{entityName}}List");
|
||||
}
|
||||
|
||||
public {{entityName}} select{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}}) {
|
||||
|
||||
return sqlSession.selectOne(MAPPER + "select{{entityName}}", {{primaryKeyProperty}});
|
||||
}
|
||||
|
||||
public void insert{{entityName}}({{entityName}} {{entityVarName}}) {
|
||||
|
||||
sqlSession.insert(MAPPER + "insert{{entityName}}", {{entityVarName}});
|
||||
}
|
||||
|
||||
public void update{{entityName}}({{entityName}} {{entityVarName}}) {
|
||||
|
||||
sqlSession.update(MAPPER + "update{{entityName}}", {{entityVarName}});
|
||||
}
|
||||
|
||||
public void delete{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}}) {
|
||||
|
||||
sqlSession.delete(MAPPER + "delete{{entityName}}", {{primaryKeyProperty}});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package {{entityPackage}};
|
||||
|
||||
{{#each entityImports}}
|
||||
import {{this}};
|
||||
{{/each}}
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class {{entityName}} {
|
||||
|
||||
{{#each columns}}
|
||||
private {{javaType}} {{propertyName}};
|
||||
|
||||
{{/each}}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="{{mapperNamespace}}">
|
||||
|
||||
<resultMap id="{{entityVarName}}ResultMap" type="{{entityFqcn}}">
|
||||
{{#each columns}}
|
||||
{{#if primaryKey}}<id property="{{propertyName}}" column="{{columnName}}"/>{{else}}<result property="{{propertyName}}" column="{{columnName}}"/>{{/if}}
|
||||
{{/each}}
|
||||
</resultMap>
|
||||
|
||||
<select id="select{{entityName}}List" resultMap="{{entityVarName}}ResultMap">
|
||||
SELECT {{#each columns}}{{columnName}}{{#unless @last}}, {{/unless}}{{/each}}
|
||||
FROM {{metadata.schema}}.{{tableName}}
|
||||
ORDER BY {{primaryKeyColumn.columnName}} DESC
|
||||
</select>
|
||||
|
||||
<select id="select{{entityName}}" parameterType="{{primaryKeyMyBatisType}}" resultMap="{{entityVarName}}ResultMap">
|
||||
SELECT {{#each columns}}{{columnName}}{{#unless @last}}, {{/unless}}{{/each}}
|
||||
FROM {{metadata.schema}}.{{tableName}}
|
||||
WHERE {{primaryKeyColumn.columnName}} = #{{'{'}}{{primaryKeyProperty}}{{'}'}}
|
||||
</select>
|
||||
|
||||
<insert id="insert{{entityName}}" parameterType="{{entityFqcn}}"{{#if primaryKeyColumn.autoIncrement}} useGeneratedKeys="true" keyProperty="{{primaryKeyProperty}}"{{/if}}>
|
||||
INSERT INTO {{metadata.schema}}.{{tableName}} (
|
||||
{{#each insertColumns}}
|
||||
{{columnName}}{{#unless @last}},{{/unless}}
|
||||
{{/each}}
|
||||
)
|
||||
VALUES (
|
||||
{{#each insertColumns}}
|
||||
#{{'{'}}{{propertyName}}{{'}'}}{{#unless @last}},{{/unless}}
|
||||
{{/each}}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="update{{entityName}}" parameterType="{{entityFqcn}}">
|
||||
UPDATE {{metadata.schema}}.{{tableName}}
|
||||
SET
|
||||
{{#each updateColumns}}
|
||||
{{columnName}} = #{{'{'}}{{propertyName}}{{'}'}}{{#unless @last}},{{/unless}}
|
||||
{{/each}}
|
||||
WHERE {{primaryKeyColumn.columnName}} = #{{'{'}}{{primaryKeyProperty}}{{'}'}}
|
||||
</update>
|
||||
|
||||
<delete id="delete{{entityName}}" parameterType="{{primaryKeyMyBatisType}}">
|
||||
DELETE FROM {{metadata.schema}}.{{tableName}}
|
||||
WHERE {{primaryKeyColumn.columnName}} = #{{'{'}}{{primaryKeyProperty}}{{'}'}}
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,18 @@
|
||||
package {{rootPackage}};
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import {{entityFqcn}};
|
||||
|
||||
public interface {{serviceName}} {
|
||||
|
||||
List<{{entityName}}> get{{entityName}}List();
|
||||
|
||||
{{entityName}} get{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}});
|
||||
|
||||
{{entityName}} create{{entityName}}({{entityName}} {{entityVarName}});
|
||||
|
||||
{{entityName}} update{{entityName}}({{entityName}} {{entityVarName}});
|
||||
|
||||
void delete{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package {{implPackage}};
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import {{rootPackage}}.{{serviceName}};
|
||||
import {{daoPackage}}.{{daoName}};
|
||||
import {{entityFqcn}};
|
||||
|
||||
@Service
|
||||
public class {{serviceImplName}} implements {{serviceName}} {
|
||||
|
||||
private final {{daoName}} {{entityVarName}}Dao;
|
||||
|
||||
public {{serviceImplName}}({{daoName}} {{entityVarName}}Dao) {
|
||||
|
||||
this.{{entityVarName}}Dao = {{entityVarName}}Dao;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<{{entityName}}> get{{entityName}}List() {
|
||||
|
||||
return {{entityVarName}}Dao.select{{entityName}}List();
|
||||
}
|
||||
|
||||
@Override
|
||||
public {{entityName}} get{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}}) {
|
||||
|
||||
return {{entityVarName}}Dao.select{{entityName}}({{primaryKeyProperty}});
|
||||
}
|
||||
|
||||
@Override
|
||||
public {{entityName}} create{{entityName}}({{entityName}} {{entityVarName}}) {
|
||||
|
||||
{{entityVarName}}Dao.insert{{entityName}}({{entityVarName}});
|
||||
return {{entityVarName}}Dao.select{{entityName}}({{entityVarName}}.{{primaryKeyGetterName}}());
|
||||
}
|
||||
|
||||
@Override
|
||||
public {{entityName}} update{{entityName}}({{entityName}} {{entityVarName}}) {
|
||||
|
||||
{{entityVarName}}Dao.update{{entityName}}({{entityVarName}});
|
||||
return {{entityVarName}}Dao.select{{entityName}}({{entityVarName}}.{{primaryKeyGetterName}}());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete{{entityName}}({{primaryKeyJavaType}} {{primaryKeyProperty}}) {
|
||||
|
||||
{{entityVarName}}Dao.delete{{entityName}}({{primaryKeyProperty}});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user