Files
SR-SYSTEM/docs/06-development-guide.md
T
2026-06-02 13:50:39 +09:00

9.9 KiB

개발 가이드 및 컨벤션 (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 최적화)

@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 패턴

@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 예외 처리 계층

// 최상위 예외
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 (이벤트 기반 알림)

// 발행
@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)

// 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 패턴

// 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)

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)

로컬 빌드 검증

# 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