init: SR System — Helpdesk & ITAM Portal
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
# 개발 가이드 및 컨벤션 (Development Guide)
|
||||
|
||||
## 1. 프로젝트 디렉토리 구조
|
||||
|
||||
### Backend (Spring Boot)
|
||||
```
|
||||
src/main/java/com/hq/support/portal/
|
||||
├── global/
|
||||
│ ├── common/
|
||||
│ │ └── ApiResponse.java // 공통 응답 래퍼
|
||||
│ ├── config/
|
||||
│ │ ├── AsyncConfig.java // @Async 스레드 풀
|
||||
│ │ ├── JpaAuditConfig.java // @EnableJpaAuditing
|
||||
│ │ ├── BatchConfig.java // Spring Batch 메타 설정
|
||||
│ │ └── SecurityConfig.java // Security Filter Chain
|
||||
│ ├── error/
|
||||
│ │ ├── ErrorCode.java // 에러코드 Enum 매트릭스
|
||||
│ │ ├── GlobalExceptionHandler.java // @RestControllerAdvice
|
||||
│ │ └── exception/
|
||||
│ │ └── BusinessException.java // 최상위 예외 클래스
|
||||
│ └── security/
|
||||
│ ├── JwtTokenProvider.java // JWT 생성/검증
|
||||
│ └── JwtAuthenticationFilter.java // 인증 필터
|
||||
│
|
||||
├── domain/
|
||||
│ ├── base/
|
||||
│ │ └── BaseEntity.java // createdAt, updatedAt, isDeleted
|
||||
│ ├── employee/
|
||||
│ │ ├── entity/TbEmployee.java
|
||||
│ │ ├── repository/EmployeeRepository.java
|
||||
│ │ └── exception/InvalidEmployeeException.java
|
||||
│ ├── project/
|
||||
│ │ ├── entity/TbProject.java
|
||||
│ │ └── repository/ProjectRepository.java
|
||||
│ ├── dispatch/
|
||||
│ │ ├── entity/TbDispatchHistory.java
|
||||
│ │ └── repository/DispatchRepository.java
|
||||
│ ├── ticket/
|
||||
│ │ ├── entity/TbIssueTicket.java
|
||||
│ │ ├── entity/TbTicketComment.java
|
||||
│ │ ├── repository/TicketRepository.java
|
||||
│ │ ├── controller/IssueTicketController.java
|
||||
│ │ ├── dto/TicketDto.java
|
||||
│ │ ├── service/IssueTicketService.java
|
||||
│ │ └── event/TicketCreatedEvent.java
|
||||
│ ├── report/
|
||||
│ │ ├── entity/TbWorkReport.java
|
||||
│ │ └── repository/ReportRepository.java
|
||||
│ ├── equipment/
|
||||
│ │ ├── entity/TbEquipment.java
|
||||
│ │ ├── entity/TbEquipmentAssignment.java
|
||||
│ │ ├── entity/TbEquipmentMaintenance.java
|
||||
│ │ └── repository/EquipmentRepository.java
|
||||
│ └── training/
|
||||
│ ├── entity/TbTrainingProgress.java
|
||||
│ └── repository/TrainingRepository.java
|
||||
│
|
||||
└── infra/
|
||||
└── notification/
|
||||
├── listener/NotificationEventListener.java
|
||||
└── scheduler/SlaJobScheduler.java
|
||||
|
||||
src/main/resources/
|
||||
├── application.yml
|
||||
└── ddl/oracle_11g_schema.sql
|
||||
```
|
||||
|
||||
### Frontend (Next.js 14+)
|
||||
```
|
||||
src/
|
||||
├── app/
|
||||
│ ├── (auth)/login/page.tsx
|
||||
│ ├── tickets/
|
||||
│ │ ├── page.tsx // 칸반 보드
|
||||
│ │ └── [ticketId]/page.tsx // 상세/타임라인
|
||||
│ ├── equipment/page.tsx // 장비 인벤토리
|
||||
│ ├── reports/page.tsx // 업무 보고서
|
||||
│ ├── training/page.tsx // 교육 진도
|
||||
│ ├── employees/page.tsx // 직원 관리 (ADMIN)
|
||||
│ └── layout.tsx // GNB + LNB 레이아웃
|
||||
├── components/
|
||||
│ ├── layout/
|
||||
│ │ ├── GNB.tsx
|
||||
│ │ └── LNB.tsx
|
||||
│ ├── ticket/
|
||||
│ │ ├── KanbanBoard.tsx
|
||||
│ │ ├── TicketCard.tsx
|
||||
│ │ └── TimelineComment.tsx
|
||||
│ ├── equipment/
|
||||
│ │ ├── EquipmentTable.tsx
|
||||
│ │ └── AssignModal.tsx
|
||||
│ └── common/
|
||||
│ ├── ApiResponse.ts
|
||||
│ └── Pagination.tsx
|
||||
├── stores/
|
||||
│ ├── authStore.ts
|
||||
│ └── notificationStore.ts
|
||||
├── services/
|
||||
│ ├── apiClient.ts // Axios 인스턴스 + 인터셉터
|
||||
│ ├── ticketService.ts
|
||||
│ ├── equipmentService.ts
|
||||
│ └── reportService.ts
|
||||
└── types/
|
||||
└── index.ts // 전역 TypeScript 타입
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Backend 코딩 컨벤션
|
||||
|
||||
### 2.1 엔티티 네이밍
|
||||
- **테이블명:** `TB_XXX` (Oracle 11g 컨벤션)
|
||||
- **엔티티 클래스명:** `TbXxx.java` (PascalCase, 테이블명 매핑)
|
||||
- **PK 컬럼:** `xxx_id` (NUMBER, Sequence 할당)
|
||||
|
||||
### 2.2 ID 생성 전략 (Oracle 11g 최적화)
|
||||
```java
|
||||
@Entity
|
||||
@SequenceGenerator(name = "SEQ_TB_TICKET", sequenceName = "SEQ_TB_TICKET", allocationSize = 50)
|
||||
public class TbIssueTicket {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_TB_TICKET")
|
||||
private Long ticketId;
|
||||
}
|
||||
```
|
||||
- Oracle DDL 시퀀스: `CREATE SEQUENCE SEQ_TB_TICKET INCREMENT BY 50;`
|
||||
- `allocationSize = 50` + `INCREMENT BY 50` 반드시 정합
|
||||
|
||||
### 2.3 Soft Delete 패턴
|
||||
```java
|
||||
@SQLDelete(sql = "UPDATE tb_issue_ticket SET is_deleted = 'Y' WHERE ticket_id = ?")
|
||||
@Where(clause = "is_deleted = 'N'")
|
||||
public class TbIssueTicket { ... }
|
||||
```
|
||||
- 모든 엔티티에 `@SQLDelete` + `@Where` 선언
|
||||
- `BaseEntity`에 `isDeleted` 컬럼 공통 상속
|
||||
|
||||
### 2.4 레이어드 아키텍처 규칙
|
||||
| 레이어 | 책임 | 규칙 |
|
||||
|--------|------|------|
|
||||
| Controller | HTTP 진입점 | DTO 반환, `@Valid` 검증, 서비스 호출만 |
|
||||
| Service | 비즈니스 로직 | `@Transactional`, 예외 throw, 이벤트 발행 |
|
||||
| Repository | DB 접근 | Spring Data JPA, `Specification` 동적 쿼리 |
|
||||
| DTO | 데이터 전송 | Request/Response 분리, 엔티티 직접 노출 금지 |
|
||||
|
||||
### 2.5 예외 처리 계층
|
||||
```java
|
||||
// 최상위 예외
|
||||
public class BusinessException extends RuntimeException {
|
||||
private final ErrorCode errorCode;
|
||||
}
|
||||
|
||||
// 도메인별 구체 예외
|
||||
public class TicketClosedException extends BusinessException {
|
||||
public TicketClosedException() {
|
||||
super(ErrorCode.CLOSED_TICKET_MOD);
|
||||
}
|
||||
}
|
||||
|
||||
// ControllerAdvice에서 ErrorCode → ApiResponse 자동 변환
|
||||
```
|
||||
|
||||
### 2.6 EDA (이벤트 기반 알림)
|
||||
```java
|
||||
// 발행
|
||||
@Service
|
||||
public class IssueTicketService {
|
||||
@Autowired
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
public TicketDto createTicket(TicketCreateReq req) {
|
||||
TbIssueTicket ticket = repository.save(req.toEntity());
|
||||
publisher.publishEvent(new TicketCreatedEvent(ticket));
|
||||
return TicketDto.from(ticket);
|
||||
}
|
||||
}
|
||||
|
||||
// 구독 (트랜잭션 완료 후 비동기 실행)
|
||||
@Component
|
||||
public class NotificationEventListener {
|
||||
@Async
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||
public void handleTicketCreated(TicketCreatedEvent event) {
|
||||
// Slack/Email 발송 (메인 트랜잭션과 완전 격리)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Frontend 코딩 컨벤션
|
||||
|
||||
### 3.1 API 통신 (Axios)
|
||||
```typescript
|
||||
// apiClient.ts - 공통 인터셉터
|
||||
const apiClient = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(res) => res.data, // ApiResponse.data 자동 언래핑
|
||||
(err) => {
|
||||
const { errorCode, message } = err.response.data;
|
||||
// errorCode 기반 전역 토스트/알림
|
||||
return Promise.reject({ errorCode, message });
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2 Pinia Store 패턴
|
||||
```typescript
|
||||
// stores/authStore.ts
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<Employee | null>(null);
|
||||
const roleCode = computed(() => user.value?.roleCode);
|
||||
|
||||
async function fetchMe() {
|
||||
const res = await apiClient.get('/employees/me');
|
||||
user.value = res.data;
|
||||
}
|
||||
|
||||
return { user, roleCode, fetchMe };
|
||||
});
|
||||
```
|
||||
|
||||
### 3.3 컴포넌트 네이밍
|
||||
- **페이지 컴포넌트:** `page.tsx` (App Router), 도메인 폴더 하위
|
||||
- **UI 컴포넌트:** `PascalCase.tsx` (단일 책임)
|
||||
- **Props:** Interface 선언 후 `defineProps<Props>()`
|
||||
- **Event Emit:** `defineEmits<{ eventName: [payload: type] }>()`
|
||||
|
||||
---
|
||||
|
||||
## 4. 개발 환경 설정
|
||||
|
||||
### Backend (application.yml)
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:oracle:thin:@localhost:1521:XE
|
||||
username: portal
|
||||
password: ${DB_PASSWORD}
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none # 운영 DDL은 별도 스크립트 관리
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.Oracle10gDialect
|
||||
batch:
|
||||
jdbc:
|
||||
initialize-schema: always # 배치 메타 테이블 자동 생성
|
||||
|
||||
jwt:
|
||||
secret: ${JWT_SECRET}
|
||||
access-token-validity: 3600000 # 1시간
|
||||
refresh-token-validity: 604800000 # 7일
|
||||
```
|
||||
|
||||
### Frontend (.env.local)
|
||||
```
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api/v1
|
||||
NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws/notifications
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 빌드 및 배포
|
||||
|
||||
### CI/CD (GitHub Actions)
|
||||
- **트리거:** `release` 생성 시에만 운영 빌드
|
||||
- **Push 이벤트:** Lint + Test만 실행 (배포 없음)
|
||||
- **파이프라인:** Build → Test → Docker Image → Deploy (Nginx Proxy Manager)
|
||||
|
||||
### 로컬 빌드 검증
|
||||
```bash
|
||||
# Backend
|
||||
./gradlew clean build
|
||||
|
||||
# Frontend
|
||||
npm run build
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Nginx Proxy Manager
|
||||
- SSL 인증서 관리
|
||||
- `/api/*` → Backend Container (Reverse Proxy)
|
||||
- `/*` → Frontend Static/Node Server
|
||||
- Rate Limit 설정 (API abuse 방어)
|
||||
|
||||
---
|
||||
|
||||
## 6. DB DDL 관리 규칙
|
||||
|
||||
- DDL 파일: `src/main/resources/ddl/oracle_11g_schema.sql`
|
||||
- 모든 시퀀스: `CREATE SEQUENCE SEQ_TB_XXX INCREMENT BY 50 START WITH 1`
|
||||
- `ddl-auto: none` — DDL은 스크립트로만 관리 (Hibernate 자동 생성 금지)
|
||||
- 스키마 변경 시 DDL 파일 버전 관리 (Git)
|
||||
- 인덱스: 자주 조회하는 컬럼(상태, 날짜, 외래키)에 인덱스 별도 추가
|
||||
- 공통 코드 데이터: `INSERT` 문으로 시드 데이터 관리
|
||||
|
||||
---
|
||||
|
||||
## 7. 커밋 컨벤션
|
||||
|
||||
한국어로 명확하게 작성 (팀 협업 가독성 보장)
|
||||
|
||||
```
|
||||
feat: SR 티켓 칸반 보드 API 구현
|
||||
fix: 장비 지급 시 낙관적 락 충돌 해결
|
||||
refactor: BaseEntity 공통 필드 추출
|
||||
docs: DB 설계서 최신화
|
||||
```
|
||||
|
||||
### 브랜치 전략
|
||||
```
|
||||
main ← develop ← feature/{domain}-{description}
|
||||
- feature/issue-ticket-kanban
|
||||
- feature/equipment-assign-optimistic-lock
|
||||
- fix/report-status-transition-bug
|
||||
```
|
||||
Reference in New Issue
Block a user