Compare commits

...

4 Commits

Author SHA1 Message Date
이진기
37bc3f423c 문서 추가 2024-12-19 13:01:04 +09:00
이진기
69d84c4706 ReadMe 추가 2024-12-06 09:15:03 +09:00
d84a3313e3 타입 변경 2024-11-30 11:40:29 +09:00
이진기
f786df1d9b /admin/content/list 페이징 처리와 검색 조건 기능 추가 2024-11-27 16:23:07 +09:00
23 changed files with 582 additions and 114 deletions

40
java/README.md Normal file
View File

@@ -0,0 +1,40 @@
### 기본 환경
- Java 11
- MyBatis 4.x
- JPA 2.2
- QueryDSL 5.0
### 개발 규약
- 사용하지 않는 Parameter는 넣지 않는다.
- Request, Response 호출은 EgovRequestUtils을 통해서만 한다.
- 관리자 Session 정보는 EgovAdminSessionUtils로 호출한다.(MultipartRequest는 제외)
- 사용자 Session 정보는 EgovUserSessionUtils로 호출한다.
- Spring context에서 벗어나게 개발하지 않는다.
### 어노테이션
- API Controller: @RestController
- Page Controller: @Controller
- Service: @Service
- Mapper: @Mapper
- DAO 또는 별도 컴포넌트들: @Component
### API URL 규약
- 대국민포털(prefix): 없음
- 참여기관/관리자포털(prefix): /admin
- 페이지: /**
- api: /api/**
- 메뉴 등록 시 full url로 기재할 것!
- 페이지 메뉴는 도메인으로 분기 처리되므로 동일 URL 사용 가능
- id 조회의 경우 반드시 query 처리할 것!
### DB 암호화
- 환경변수 추가 필요(안하면 에러 남)
- {project.baseDir}/main/resources/ksing/db 에 있는 모든 파일을 C:\SecureDBAgent 에 복사
- 아래의 정보로 환경변수 세팅 진행
- SDB_HOME / C:\SecureDBAgent
- SDB_FIRST_PORT / 9909
### 비지니스 로직 로깅 관련
- [필수] 비지니스 로직에 로깅할 경우 class 상단에 @Slf4j 어노테이션 선언
- [필수] Service에서는 전자정부프레임워크에서 제공하는 egovLogger를 사용

29
java/git.md Normal file
View File

@@ -0,0 +1,29 @@
### 임시 git 설정(SSH 터널링)
```
ssh -i KLAC_SYS_ADM.pem -o ServerAliveInterval=60 -L 8081:localhost:8081 -L 5000:localhost:3000 rocky@192.168.30.7 -p 6722
```
### 브랜치 용도
- master: 운영 서버 반영 브랜치
- dev: 개발 서버 반영 브랜치
- mix: 개발용 브랜치
### 신규 브랜치 작성 방법
- feature/gitea아이디/번호
- 예) feature/natoro/1
- push 할 때마다 뒷 번호는 증가
### git 사용 방법
1. 작업하기 전 반드시 mix 브랜치에서 new branch로 생성
2. 작업이 완료될 경우 반드시 commit
3. 새로운 내용을 받을 때 pull(예: mix 브랜치 pull)
4. 작업한 내용을 서버에 올릴 때 push
5. 개발자는 merge 작업하지 말 것!
6. 작업 단위는 작게 진행(기능별로 분리해서 branch 작업)
7. 커미터, 기능별 태그 관리 및 버전 관리할 것
### 소스 관리 및 반영 관리(수정)
1. pull request 처리 주기
2. 코드 리뷰 주기
3. 반영 주기
4. 예) 수요일 - 오후 4시 pull request 처리

8
java/mapper.md Normal file
View File

@@ -0,0 +1,8 @@
Mapper 사용 시 아래와 같이 작성(중괄호는 치환되는 명칭)
```
package: egovframework.com.lasp.{업무명}.mapper
Mapper파일명: {Admin | User}{업무명}Mapper.xml
annotation: org.egovframe.rte.psl.dataaccess.mapper.Mapper
xml파일: \resources\egovframework\sqlmap\mappers\{Mapper명과 동일}.xml
```

View File

@@ -1,12 +1,16 @@
package com.leejk0523.javavue.admin.contents.dao;
import ch.qos.logback.core.util.StringUtil;
import com.leejk0523.javavue.admin.contents.vo.ContentsListResult;
import com.leejk0523.javavue.admin.contents.vo.ContentsPagingQuery;
import com.leejk0523.javavue.common.QueryDSLUtils;
import com.leejk0523.javavue.model.AsaContent;
import com.leejk0523.javavue.model.QAsaContent;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import io.micrometer.common.util.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
@@ -18,6 +22,7 @@ import java.util.List;
@Repository
public class AdminContentsDao extends QuerydslRepositorySupport {
public AdminContentsDao() {
super(AsaContent.class);
}
@@ -25,12 +30,33 @@ public class AdminContentsDao extends QuerydslRepositorySupport {
public Page<ContentsListResult> findContentsList(ContentsPagingQuery query) {
QAsaContent asaContent = QAsaContent.asaContent;
final var offset = getOffset(query);
final var limit = getLimit(query);
final var pageable = getPageable(query);
final var offset = QueryDSLUtils.getOffset(query);
final var limit = QueryDSLUtils.getLimit(query);
final var pageable = QueryDSLUtils.getPageable(query);
BooleanExpression expression = asaContent.delYn.eq("N");
if (StringUtils.isNotEmpty(query.getKeyword())) {
switch (query.getType()) {
case CONTENT: {
expression = expression.and(asaContent.contents.contains(query.getKeyword()));
break;
}
case TITLE : {
expression = expression.and(asaContent.contentTitle.contains(query.getKeyword()));
break;
}
case TOTAL: {
expression = expression.and(asaContent.contents.contains(query.getKeyword())
.or(asaContent.contentTitle.contains(query.getKeyword())));
}
}
}
if (StringUtils.isNotEmpty(query.getOrgId())) {
expression = expression.and(asaContent.orgId.eq(query.getOrgId()));
}
final var list = from(asaContent)
.select(
Projections.bean(
@@ -55,17 +81,4 @@ public class AdminContentsDao extends QuerydslRepositorySupport {
return new PageImpl<>(list, pageable, total);
}
private Pageable getPageable(ContentsPagingQuery query) {
return PageRequest.of(query.getPage() - 1, query.getSize());
}
private long getOffset(ContentsPagingQuery query) {
return (long) (query.getPage() - 1) * query.getSize();
}
private long getLimit(ContentsPagingQuery query) {
return query.getSize();
}
}

View File

@@ -1,10 +1,11 @@
package com.leejk0523.javavue.admin.contents.vo;
import com.leejk0523.javavue.common.PagingQuery;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
@Data
public class ContentsPagingQuery {
public class ContentsPagingQuery implements PagingQuery {
@NotNull
private int page;
@@ -19,6 +20,7 @@ public class ContentsPagingQuery {
public enum Type {
TITLE,
CONTENT
CONTENT,
TOTAL
}
}

View File

@@ -0,0 +1,48 @@
package com.leejk0523.javavue.code.dao;
import com.leejk0523.javavue.common.GridCode;
import com.leejk0523.javavue.model.ComCd;
import com.leejk0523.javavue.model.QAsaSite;
import com.leejk0523.javavue.model.QIstInst;
import com.querydsl.core.types.Projections;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class CodeDAO extends QuerydslRepositorySupport {
public CodeDAO() {
super(ComCd.class);
}
public List<GridCode> findSiteCodeList() {
final var site = QAsaSite.asaSite;
return from(site)
.select(
Projections.bean(
GridCode.class,
site.siteId.as("value"),
site.siteName.as("text"),
site.siteName.as("label")
)
)
.fetch();
}
public List<GridCode> findInstCodeList() {
final var inst = QIstInst.istInst;
return from(inst)
.select(
Projections.bean(
GridCode.class,
inst.instNo.as("value"),
inst.instNm.as("label"),
inst.instNm.as("text")
)
)
.fetch();
}
}

View File

@@ -0,0 +1,10 @@
package com.leejk0523.javavue.code.service;
import com.leejk0523.javavue.common.GridCode;
import java.util.List;
public interface CodeService {
List<GridCode> findSiteCodeList();
List<GridCode> findInstCodeList();
}

View File

@@ -0,0 +1,27 @@
package com.leejk0523.javavue.code.service.impl;
import com.leejk0523.javavue.code.dao.CodeDAO;
import com.leejk0523.javavue.code.service.CodeService;
import com.leejk0523.javavue.common.GridCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("lasp.codeServiceImpl")
@RequiredArgsConstructor
public class CodeServiceImpl implements CodeService {
private final CodeDAO codeDAO;
@Override
public List<GridCode> findSiteCodeList() {
return codeDAO.findSiteCodeList();
}
@Override
public List<GridCode> findInstCodeList() {
return codeDAO.findInstCodeList();
}
}

View File

@@ -0,0 +1,29 @@
package com.leejk0523.javavue.code.web;
import com.leejk0523.javavue.code.service.CodeService;
import com.leejk0523.javavue.common.GridCode;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class CodeController {
private final CodeService codeService;
@GetMapping("/api/admin/code/siteList")
public ResponseEntity<List<GridCode>> siteCodeList() {
final var results = codeService.findSiteCodeList();
return ResponseEntity.ok(results);
}
@GetMapping("/api/admin/code/instList")
public ResponseEntity<List<GridCode>> instCodeList() {
final var results = codeService.findInstCodeList();
return ResponseEntity.ok(results);
}
}

View File

@@ -0,0 +1,10 @@
package com.leejk0523.javavue.common;
import lombok.Data;
@Data
public class GridCode {
private String label;
private String text;
private String value;
}

View File

@@ -0,0 +1,6 @@
package com.leejk0523.javavue.common;
public interface PagingQuery {
int getPage();
int getSize();
}

View File

@@ -0,0 +1,18 @@
package com.leejk0523.javavue.common;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
public class QueryDSLUtils {
public static Pageable getPageable(PagingQuery query) {
return PageRequest.of(query.getPage() - 1, query.getSize());
}
public static long getOffset(PagingQuery query) {
return (long) (query.getPage() - 1) * query.getSize();
}
public static long getLimit(PagingQuery query) {
return query.getSize();
}
}

View File

@@ -0,0 +1,110 @@
package com.leejk0523.javavue.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "com_cd")
@IdClass(ComCd.Key.class)
public class ComCd {
@Id
@NotNull
@Size(max = 20)
@Column(name = "CD_GROUP_ID", nullable = false, length = 20)
private String cdGroupId;
@Id
@Size(max = 6)
@NotNull
@Column(name = "COM_CD", nullable = false, length = 6)
private String comCd;
@Size(max = 100)
@Column(name = "COM_CD_NM", length = 100)
private String comCdNm;
@Size(max = 2000)
@Column(name = "COM_CD_EXPLN", length = 2000)
private String comCdExpln;
@Column(name = "SORT_SEQ", nullable = false)
private Integer sortSeq;
@Column(name = "USE_YN", length = 1, nullable = false)
private String useYn;
@Column(name = "DEL_YN", length = 1, nullable = false)
private String delYn;
@Size(max = 20)
@Column(name = "ARTCL_NM1", length = 20)
private String artclNm1;
@Size(max = 20)
@Column(name = "ARTCL_NM2", length = 20)
private String artclNm2;
@Size(max = 20)
@Column(name = "ARTCL_NM3", length = 20)
private String artclNm3;
@Size(max = 20)
@Column(name = "ARTCL_NM4", length = 20)
private String artclNm4;
@Size(max = 20)
@Column(name = "ARTCL_NM5", length = 20)
private String artclNm5;
@Size(max = 20)
@Column(name = "ARTCL_NM6", length = 20)
private String artclNm6;
@Size(max = 20)
@Column(name = "ARTCL_NM7", length = 20)
private String artclNm7;
@Size(max = 20)
@Column(name = "ARTCL_NM8", length = 20)
private String artclNm8;
@Size(max = 20)
@Column(name = "ARTCL_NM9", length = 20)
private String artclNm9;
@Size(max = 20)
@Column(name = "ARTCL_NM10", length = 20)
private String artclNm10;
@Column(name = "FRST_RGTR_ID", length = 50, nullable = false)
private String frstRgtrId;
@Column(name = "FRST_REG_DT", nullable = false)
private LocalDateTime frstRegDt;
@Column(name = "LAST_MDFR_ID", length = 50)
private String lastMdfrId;
@Column(name = "LAST_MDFCN_DT")
private LocalDateTime lastMdfcnDt;
@Data
public static class Key implements Serializable {
private static final long serialVersionUID = -3281903370957728656L;
private String cdGroupId;
private String comCd;
}
}

View File

@@ -0,0 +1,52 @@
package com.leejk0523.javavue.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "IST_INST")
public class IstInst {
@Id
@Column(name = "INST_NO", length = 10, nullable = false)
private String instNo;
@Column(name = "INST_CLSF_CD", length = 6, nullable = false)
private String instClsfCd;
@Column(name = "INST_NM", length = 200, nullable = false)
private String instNm;
@Column(name = "INST_SRVC_EXPLN", length = 50)
private String instSrvcExpln;
@Column(name = "INST_CN")
private String instCn;
@Column(name = "ATCH_FILE_ID", length = 20)
private String atchFileId;
@Column(name = "USE_YN", length = 1, nullable = false)
private String useYn;
@Column(name = "FRST_RGTR_ID", length = 50, nullable = false)
private String frstRgtrId;
@Column(name = "FRST_REG_DT", nullable = false)
private LocalDateTime frstRegDt;
@Column(name = "LAST_MDFR_ID", length = 50)
private String lastMdfrId;
@Column(name = "LAST_MDFCN_DT")
private LocalDateTime lastMdfcnDt;
}

View File

@@ -7,4 +7,16 @@ spring.datasource.password=Ghtkssk0325
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show-sql=true
# SQL 출력
#spring.jpa.show-sql=true
# SQL 포맷팅
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.type=trace
# 바인딩 파라미터 값 로깅
logging.level.org.hibernate.type.descriptor.sql=TRACE
# SQL에 바인딩된 파라미터 값도 출력
logging.level.org.hibernate.SQL=DEBUG

View File

@@ -1,75 +1,71 @@
# Nuxt Minimal Starter
### 기본 환경
- vue 3.x(5.0.8)
- node (v20.17.0)
- vue-router(최신)
- pinia(최신) - store
- Nuxt(최신) - 프레임워크
- antd 4.x - UI/UX
- yarn (1.22.22)
- 추후 환경은 변경될 수 있음
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## 최초 세팅
- terminal(혹은 command창): yarn install
- 실행 시 yarn dev
## Setup
## 개발 시 중요 사항
- 모든 파일은 업무 기준으로 작성됨(예: 사용자 > user)
- 공통사항으로 적용되는 경우 utils 사용할 것!
- 모든 네이밍은 명확한 단어로 사용할 것!
- 반드시 저장할 경우 Eslint, Prettier 적용
Make sure to install dependencies:
## 공통 단어 주의사항
- 비즈니스 로직은 공통이 아님(불허)
- 공통은 언제든지 사용할 수 있는 라이브러리성을 말하는 것임
```bash
# npm
npm install
## Commit 금지 파일 및 디렉토리
- /node_modules/
- 기타 IDE 환경 파일(.classpath 등등)
# pnpm
pnpm install
### 페이지 작성
- pages/도메인/index.vue
- 페이지는 controller와 같은 역할을 함
- useHead를 이용하여 html head 영역을 설정함
# yarn
yarn install
### 페이지 내 컴포넌트
- /src/components/도메인/컴포넌트명.vue
# bun
bun install
### store 작성 위치
- 디렉토리: /src/stores/도메인/index.ts
### api 작성 위치
- 디렉토리: /src/apis/도메인/index.ts
### Type 지정
- type 및 interface로 타입을 지정하는 경우가 많음
- 법률구조공단은 type으로 모든 객체의 타입을 지정하는 것을 원칙으로 함
- 최대한 undefined를 사용하지 않는 선으로 개발할 것
- 예로 userId: string의 경우 빈값을 표현할 때 userId: '' 형식으로 사용할 것!
- 타입 import 시 아래와 같이 type을 지정해야 함
```
import type {UserItemType} from 'types'
```
## Development Server
Start the development server on `http://localhost:3000`:
### Toast UI Calendar
```
// ES MODULE
import Calendar from '@toast-ui/calendar';
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
// CSS 적용
import '@toast-ui/calendar/dist/toastui-calendar.min.css';
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
### Toast UI Grid
```
// ES MODULE
import 'tui-grid/dist/tui-grid.css';
import 'tui-date-picker/dist/tui-date-picker.min.css';
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
// CSS 적용
import Grid from 'tui-grid';
```

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import type { GridCodeType } from '~/types';
const value = defineModel<string>({ default: '' });
const props = defineProps<{
options: GridCodeType[];
className?: string;
selectType?: 'SELECT' | 'ALL';
isLoading?: boolean;
}>();
const selectOptions = computed(() => {
if (props.selectType === 'REQUIRED') {
return [{ label: '선택', value: '' }, ...props.options];
}
if (props.selectType === 'ALL') {
return [{ label: '전체', value: '' }, ...props.options];
}
return props.options;
});
const emit = defineEmits(['change']);
const change = () => {
emit('change');
};
</script>
<template>
<a-select
:class="className"
:options="selectOptions"
:loading="isLoading"
v-model:value="value"
@change="change"
/>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
const props = defineProps<{
className?: string;
selectType?: 'SELECT' | 'ALL';
}>();
const commonCodeStore = useCommonCodeStore();
const value = defineModel<string>('');
const { data, isLoading } = useQuery({
queryKey: ['INST_CODE_LIST'],
queryFn: async () => {
return await commonCodeStore.searchInstCodeList();
},
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
refetchOnMount: false
});
</script>
<template>
<common-default-select-code
v-if="!isLoading"
:class-name="className"
:select-type="selectType"
:options="data"
v-model="value"
/>
</template>

View File

@@ -69,7 +69,7 @@ const nonValid = computed(() => {
:colon="false"
:label-col="{ span: 2 }"
>
<!-- <common-inst-code-select v-model:value="contents.orgId" />-->
<common-inst-code-select v-model:value="contents.orgId" />
</a-form-item>
</a-col>

View File

@@ -2,13 +2,11 @@
import type { OptColumn, OptRowHeader } from 'tui-grid/types/options';
import { useContentStore } from '~/stores/contents';
import type { ContentType } from '~/types/contents';
import { BOOLEANS } from '~/constants/grid';
// import { useCommonCodeStore } from '~/stores';
import { useCommonCodeStore } from '~/stores';
const router = useRouter();
// const siteCodeList = await useCommonCodeStore().searchSiteCodeList();
// const instCodeList = await useCommonCodeStore().searchInstCodeList();
const instCodeList = await useCommonCodeStore().searchInstCodeList();
const contentStore = useContentStore();
const { contentsList, contentsQuery } = storeToRefs(contentStore);
const gridRef = ref();
@@ -28,6 +26,15 @@ const columns: OptColumn[] = [
name: 'orgId',
header: '관리기관',
width: 100,
disabled: true,
formatter: 'listItemText',
resizable: true,
editor: {
type: 'select',
options: {
listItems: instCodeList
}
}
},
{
name: 'useYn',
@@ -58,7 +65,7 @@ const columns: OptColumn[] = [
];
const contentType = [
{ label: '전체', value: '' },
{ label: '전체', value: 'TOTAL' },
{ label: '제목', value: 'TITLE' },
{ label: '내용', value: 'CONTENT' }
];
@@ -72,7 +79,7 @@ onBeforeUnmount(() => {
});
const search = () => {
// contentStore.searchContentList();
contentStore.searchContentList();
};
const list = computed(() => {
@@ -133,12 +140,12 @@ const editPage = (contentId: any) => {
<a-col>
<a-space>
<a-typography-text>관리기관</a-typography-text>
<!-- <lazy-common-inst-code-select-->
<!-- key="inst-code-select"-->
<!-- v-model="contentsQuery.orgId"-->
<!-- class-name="w-40"-->
<!-- select-type="ALL"-->
<!-- />-->
<lazy-common-inst-code-select
key="inst-code-select"
v-model="contentsQuery.orgId"
class-name="w-40"
select-type="ALL"
/>
</a-space>
</a-col>
@@ -151,7 +158,8 @@ const editPage = (contentId: any) => {
v-model:value="contentsQuery.type"
:options="contentType"
/>
<a-input title="검색어" placeholder="Search" class="w-60" />
<a-input title="검색어" placeholder="Search" class="w-60"
v-model:value="contentsQuery.keyword" allow-clear/>
</a-space>
</a-col>
<a-col>

View File

@@ -16,7 +16,7 @@ const DEFAULT_CONTENT_QUERY: ContentListQueryType = {
page: 1,
siteId: '',
size: 10,
type: ''
type: 'TOTAL'
};
const DEFAULT_CONTENTS_LIST: Page<ContentListType> = {

View File

@@ -60,16 +60,6 @@ export const useDefaultStore = defineStore('useDefaultStore', () => {
});
export const useCommonCodeStore = defineStore('useCommonCodeStore', () => {
const searchCommonCodeList = async (
codeGroupId: string
): Promise<GridCodeType[]> => {
const { data } = await useAxios().get('/api/admin/code/codeList', {
params: {
codeGroupId
}
});
return data;
};
const searchSiteCodeList = async (): Promise<GridCodeType[]> => {
const { data } = await useAxios().get('/api/admin/code/siteList');
@@ -83,17 +73,8 @@ export const useCommonCodeStore = defineStore('useCommonCodeStore', () => {
return data;
};
const searchRoleCodeList = async (): Promise<GridCodeType[]> => {
const { data } = await useAxios().get<GridCodeType[]>(
'/api/admin/code/roleList'
);
return data;
};
return {
searchInstCodeList,
searchSiteCodeList,
searchCommonCodeList,
searchRoleCodeList
searchSiteCodeList
};
});

View File

@@ -4,7 +4,7 @@ export type ContentListQueryType = {
siteId: string;
orgId: string;
keyword: string;
type: '' | 'TITLE' | 'CONTENT';
type: 'TOTAL' | 'TITLE' | 'CONTENT';
};
export type ContentListType = {