init: SR System — Helpdesk & ITAM Portal

This commit is contained in:
pray4skylark
2026-06-02 13:50:39 +09:00
commit 832c9f096f
262 changed files with 30470 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules
.git
.next
*.md
backend/
+7
View File
@@ -0,0 +1,7 @@
# PostgreSQL
POSTGRES_DB=sr_system
POSTGRES_USER=sr_admin
POSTGRES_PASSWORD=<your-db-password>
# JWT (최소 256bit)
JWT_SECRET=<your-256bit-jwt-secret>
+47
View File
@@ -0,0 +1,47 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
backend/build/
backend/.gradle/
backend/out/
*.jar
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files
.env
.env.local
.env.production
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+47
View File
@@ -0,0 +1,47 @@
# Agent Tier Routing Guide
3-tier agent system: **LITE → MID → MAX**
## Decision Flow
1. **LITE first** for simple/cheap tasks
2. **MID** for normal complexity
3. **MAX** only when MID fails 3+ times
## Per-role routing
### Architecture
| Task | Agent |
|------|-------|
| Simple design sketch, brief review | @architect-lite (LITE) |
| Full architecture, domain modeling, DB schema | @architect (MID) |
| Unsolvable architecture problem | @escalation (MAX) |
### Code Analysis
| Task | Agent |
|------|-------|
| Quick file find, simple grep | @context-manager-lite (LITE) |
| Deep codebase analysis, refactoring scope | @context-manager (MID) |
| Beyond MID capability | @escalation (MAX) |
### Implementation
| Task | Agent |
|------|-------|
| DTO/Entity boilerplate, simple CRUD, basic test | @worker-lite (LITE) |
| Business logic, API binding, complex test | @worker (MID) |
| Beyond MID capability | @escalation (MAX) |
### Error Fixing
| Task | Agent |
|------|-------|
| Missing import, syntax fix, format lint | @inspector-lite (LITE) |
| Multi-step compile error, runtime bug | @inspector (MID) |
| 3+ failures on same bug | @escalation (MAX) |
## Cost Awareness
- LITE: deepseek-v4-flash / mimo-v2.5 — $0.14/$0.28 per 1M tokens
- MID: deepseek-v4-pro / mimo-v2.5-pro / qwen3.6-plus — $0.50~$1.74/$3.48
- MAX: glm-5.1 — $1.40/$4.40 (880 req/5hr limit, use sparingly)
## Rule
Never use MAX for problems that LITE or MID can handle. Let the failed agent's prompt guide escalation.
+27
View File
@@ -0,0 +1,27 @@
# Token Efficiency — Caveman Default
Load skill: `~/.agents/skills/caveman/SKILL.md`
## Default Mode
Output in **caveman full** level. Exceptions:
| Context | Mode | Reason |
|---------|------|--------|
| Code/blocks | Normal | Accuracy critical |
| Commit msg | Normal | Conventional commits |
| Security warnings | Normal | Clarity required |
| Irreversible confirm | Normal | Safety |
| User asks "normal mode" | Normal | User override |
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms. Technical terms exact. Code blocks unchanged. Errors quoted exact.
Pattern: `[thing] [action] [reason]. [next step].`
## Intensity Switching
- `/caveman lite` — no filler/hedging, keep sentences
- `/caveman full` — default, classic caveman
- `/caveman ultra` — max compression
- `stop caveman` / `normal mode` — revert
+90
View File
@@ -0,0 +1,90 @@
# Context Caching Optimization Specification
## 1. Prompt Structure (Strict)
Prompts MUST follow this sequence:
### [A. STATIC REGION] - Top (Always Cached)
1. System Persona & Rules (e.g., .geminirules)
2. Background Knowledge (API specs, architecture, DB schema)
3. Output Format Definitions
### [B. DYNAMIC REGION] - Bottom (Cache Boundary)
1. Variable Parameters (Current Time, Session Metadata)
2. User Request / Query
## 2. Token Compression
- Data Flattening: Use Markdown tables, CSV, or JSON instead of prose
- Metadata Sanitization: Remove code comments, redundant empty lines, duplicate spacing, and non-essential tracking properties
## 3. Session Controls
- History Truncation: Do not concatenate raw dialog turns (invalidates cache)
- Checkpoint Aggregation: Summarize history at checkpoints; cache summary, flush raw turns
- Request Batching: Align multi-agent tasks within same TTL cycles for Cache Hit ratio
# Agentic Coding Workflow - Model Orchestration
Role-based model dispatching strategy for rate limit optimization.
## 1. Agent Architecture
4-layer workflow routing by context size, reasoning difficulty, speed, and remaining quota:
- Layer 1. Architect Agent (Design / Bug Tracking)
Models: DeepSeek V4 Pro, Qwen3.6 Plus (Sub: GLM-5.1)
Role: Requirements analysis and structural design specification
- Layer 2. Repository Context Agent (Large-scale Source Analysis)
Models: MiMo-V2.5-Pro, Kimi K2.6
Role: Long context codebase analysis and task splitting
- Layer 3. Worker Agent (Code Generation / Boilerplate)
Models: Qwen3.5 Plus, MiniMax M2.7
Role: Business logic and function-level implementation
- Layer 4. Inspector Agent (Compile/Lint Error Self-Correction)
Models: DeepSeek V4 Flash
Role: Build monitoring, syntax error fix, output format validation
## 2. Layer Routing & Limits
| Layer | Models | Hourly | Weekly | Primary Tasks |
|---|---|---|---|---|
| Architect | DeepSeek V4 Pro, Qwen3.6 Plus, GLM-5.1 | 3450, 3300, 880 | 8550, 8200, 2150 | Domain modeling, DB schema, complex debugging. GLM-5.1: approval only |
| Context Manager | MiMo-V2.5-Pro, Kimi K2.6 | 1290, 1150 | 3225, 2880 | Long context analysis, legacy refactoring scope, API doc ingestion |
| Worker | Qwen3.5 Plus, MiniMax M2.7 | 10200, 3400 | 25200, 8500 | Unit function impl, API binding, DTO/Entity boilerplate, unit tests |
| Inspector | DeepSeek V4 Flash | 31650 | 79050 | Compile/lint error fix, output validation, self-correction loop |
## 3. Implementation Guide
```python
architect_llm = CustomLLM("deepseek-v4-pro")
context_llm = CustomLLM("mimo-v2.5-pro")
worker_llm = CustomLLM("qwen-3.5-plus")
inspector_llm = CustomLLM("deepseek-v4-flash")
architect = Agent(role="Software Architect", llm=architect_llm)
code_worker = Agent(role="Senior Backend Developer", llm=worker_llm)
linter_inspector = Agent(role="QA & Code Inspector", llm=inspector_llm)
```
## 4. Operational Rules
1. DeepSeek V4 Flash Firewall
- Typos, semicolons, import errors: NEVER escalate to higher models
- Minimum 3 self-correction loops must resolve within Flash (31650/hr)
2. GLM-5.1 Escalation Threshold
- 880/hr limit is tight. Exclude from default pipeline
- Use only as last-resort fallback after 3+ failures from other models
3. Context Split for Token Savings
- After MiMo/Kimi scans full repo, extract only needed snippets for Qwen3.5 Plus
4. MiniMax Surge Switching
- During boilerplate/test code surges, temporarily route Worker from Qwen3.5 Plus to MiniMax M2.5 (6300/hr) or M2.7 to prevent weekly/monthly lock
## 5. Development Docs
1. Base docs: ./docs/
2. Real-time dev logs: ./work_logs/
3. New session: check ./work_logs/work_log_*.md to resume
4. Milestone complete: use /graphify to map milestone relationships
5. Design elements: ./premium_enterprise/DESIGN.md
+17
View File
@@ -0,0 +1,17 @@
FROM node:20-alpine AS build
ARG API_BACKEND_URL
ENV API_BACKEND_URL=$API_BACKEND_URL
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "server.js"]
+5
View File
@@ -0,0 +1,5 @@
.gradle
build
*.md
.gradle/
build/
+13
View File
@@ -0,0 +1,13 @@
FROM eclipse-temurin:17-jdk AS build
WORKDIR /app
COPY gradlew build.gradle settings.gradle ./
COPY gradle gradle
RUN chmod +x gradlew && ./gradlew dependencies --no-daemon
COPY src src
RUN ./gradlew bootJar --no-daemon
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
+45
View File
@@ -0,0 +1,45 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
implementation 'org.flywaydb:flyway-core'
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation 'org.postgresql:postgresql'
implementation 'org.projectlombok:lombok:1.18.38'
annotationProcessor 'org.projectlombok:lombok:1.18.38'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
useJUnitPlatform()
}
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+82
View File
@@ -0,0 +1,82 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'sr-system-backend'
@@ -0,0 +1,12 @@
package com.srsystem;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SrsystemApplication {
public static void main(String[] args) {
SpringApplication.run(SrsystemApplication.class, args);
}
}
@@ -0,0 +1,67 @@
package com.srsystem.batch;
import com.srsystem.dto.NotificationEvent;
import com.srsystem.entity.Ticket;
import com.srsystem.entity.enums.TicketStatus;
import com.srsystem.listener.NotificationPublisher;
import com.srsystem.repository.TicketRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Component
@RequiredArgsConstructor
@Slf4j
public class SlaExpirationJob {
private final TicketRepository ticketRepository;
private final NotificationPublisher notificationPublisher;
private static final List<TicketStatus> EXCLUDED_STATUSES = List.of(TicketStatus.RESOLVED, TicketStatus.CLOSED);
@Transactional
public int processExpiredTickets() {
List<Ticket> expiredTickets = ticketRepository
.findBySlaWarnFlagAndStatusNotIn('N', EXCLUDED_STATUSES);
int count = 0;
for (Ticket ticket : expiredTickets) {
if (ticket.getDueDate() != null && ticket.getDueDate().isBefore(LocalDateTime.now())) {
ticket.setSlaWarnFlag('Y');
ticketRepository.save(ticket);
if (ticket.getRequester() != null) {
notificationPublisher.publish(new NotificationEvent(
"SLA_WARNING",
"SLA Expired: Ticket #" + ticket.getTicketId(),
"Ticket \"" + ticket.getTitle() + "\" has exceeded its SLA deadline.",
"/issues/" + ticket.getTicketId(),
ticket.getRequester().getEmpId()
));
}
if (ticket.getAssignee() != null) {
notificationPublisher.publish(new NotificationEvent(
"SLA_WARNING",
"SLA Expired: Ticket #" + ticket.getTicketId(),
"Assigned ticket \"" + ticket.getTitle() + "\" has exceeded its SLA deadline.",
"/issues/" + ticket.getTicketId(),
ticket.getAssignee().getEmpId()
));
}
count++;
}
}
if (count > 0) {
log.info("SLA Expiration Job: {} tickets marked as overdue", count);
}
return count;
}
}
@@ -0,0 +1,25 @@
package com.srsystem.batch;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class SlaScheduler {
private final SlaExpirationJob slaExpirationJob;
@Scheduled(cron = "0 0 * * * *")
public void runSlaCheck() {
log.info("SLA Scheduler: Starting hourly SLA expiration check");
try {
int processed = slaExpirationJob.processExpiredTickets();
log.info("SLA Scheduler: Completed. {} tickets processed.", processed);
} catch (Exception e) {
log.error("SLA Scheduler: Error during SLA check", e);
}
}
}
@@ -0,0 +1,9 @@
package com.srsystem.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
}
@@ -0,0 +1,9 @@
package com.srsystem.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class BatchConfig {
}
@@ -0,0 +1,10 @@
package com.srsystem.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
@@ -0,0 +1,75 @@
package com.srsystem.config;
import com.srsystem.config.jwt.JwtAuthenticationFilter;
import com.srsystem.config.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**", "/api/v1/health", "/api/v1/wake").permitAll()
.requestMatchers("/api/v1/**").authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@@ -0,0 +1,34 @@
package com.srsystem.config;
import com.srsystem.config.jwt.CurrentUserArgumentResolver;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final CurrentUserArgumentResolver currentUserArgumentResolver;
public WebConfig(CurrentUserArgumentResolver currentUserArgumentResolver) {
this.currentUserArgumentResolver = currentUserArgumentResolver;
}
@Value("${file.upload-dir:./uploads}")
private String uploadDir;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserArgumentResolver);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + uploadDir + "/");
}
}
@@ -0,0 +1,11 @@
package com.srsystem.config.jwt;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
@@ -0,0 +1,47 @@
package com.srsystem.config.jwt;
import com.srsystem.entity.Employee;
import com.srsystem.service.AuthService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Slf4j
@Component
@RequiredArgsConstructor
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
private final AuthService authService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class) &&
parameter.getParameterType().equals(Employee.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
log.warn("No authentication found");
return null;
}
String loginId = authentication.getName();
log.info("Resolving current user for loginId: {}", loginId);
Employee emp = authService.getEmployeeByLoginId(loginId);
if (emp == null) {
log.warn("Employee not found for loginId: {}", loginId);
return null;
}
log.info("Found employee: empId={}, name={}", emp.getEmpId(), emp.getName());
return emp;
}
}
@@ -0,0 +1,56 @@
package com.srsystem.config.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
String loginId = jwtTokenProvider.getLoginId(token);
String role = jwtTokenProvider.getRole(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
loginId,
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("Security context set for user: {}", loginId);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
@@ -0,0 +1,90 @@
package com.srsystem.config.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Slf4j
@Component
public class JwtTokenProvider {
private final SecretKey secretKey;
private final long accessTokenExpiration;
private final long refreshTokenExpiration;
public JwtTokenProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.access-token-expiration}") long accessTokenExpiration,
@Value("${jwt.refresh-token-expiration}") long refreshTokenExpiration) {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
this.accessTokenExpiration = accessTokenExpiration;
this.refreshTokenExpiration = refreshTokenExpiration;
}
public String generateAccessToken(String loginId, String role) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenExpiration);
return Jwts.builder()
.subject(loginId)
.claim("role", role)
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
public String generateRefreshToken(String loginId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + refreshTokenExpiration);
return Jwts.builder()
.subject(loginId)
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
public String getLoginId(String token) {
return getClaims(token).getSubject();
}
public String getRole(String token) {
return getClaims(token).get("role", String.class);
}
public boolean validateToken(String token) {
try {
getClaims(token);
return true;
} catch (ExpiredJwtException e) {
log.warn("Expired JWT token: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.warn("Unsupported JWT token: {}", e.getMessage());
} catch (MalformedJwtException e) {
log.warn("Malformed JWT token: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.warn("Invalid JWT token: {}", e.getMessage());
}
return false;
}
private Claims getClaims(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
}
@@ -0,0 +1,39 @@
package com.srsystem.controller;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.LoginRequest;
import com.srsystem.dto.LoginResponse;
import com.srsystem.dto.RefreshTokenRequest;
import com.srsystem.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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;
@Tag(name = "Authentication", description = "인증 관련 API")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@Operation(summary = "로그인", description = "아이디와 비밀번호로 로그인하여 액세스 토큰과 리프레시 토큰을 발급받습니다")
@PostMapping("/login")
public ResponseEntity<ApiResponse<LoginResponse>> login(@Valid @RequestBody LoginRequest request) {
LoginResponse response = authService.login(request);
return ResponseEntity.ok(ApiResponse.ok(response));
}
@Operation(summary = "토큰 갱신", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급받습니다")
@PostMapping("/refresh")
public ResponseEntity<ApiResponse<LoginResponse>> refresh(@RequestBody RefreshTokenRequest request) {
LoginResponse response = authService.refresh(request.refreshToken());
return ResponseEntity.ok(ApiResponse.ok(response));
}
}
@@ -0,0 +1,40 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.CalendarEvent;
import com.srsystem.entity.Employee;
import com.srsystem.service.CalendarService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@Tag(name = "Calendar", description = "통합 일정 조회 API")
@RestController
@RequestMapping("/api/v1/calendar")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class CalendarController {
private final CalendarService calendarService;
@Operation(summary = "통합 일정 조회", description = "기간 내 모든 일정 (티켓 마감, 보고서 제출, 파견 기간) 을 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/events")
public ResponseEntity<ApiResponse<List<CalendarEvent>>> getEvents(
@RequestParam String start,
@RequestParam String end,
@CurrentUser Employee currentUser) {
LocalDate startDate = LocalDate.parse(start);
LocalDate endDate = LocalDate.parse(end);
List<CalendarEvent> events = calendarService.getEvents(currentUser.getEmpId(), currentUser.getRoleCode(), startDate, endDate);
return ResponseEntity.ok(ApiResponse.ok(events));
}
}
@@ -0,0 +1,64 @@
package com.srsystem.controller;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.CommonCodeRequest;
import com.srsystem.entity.CommonCode;
import com.srsystem.service.CommonCodeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "Common Code", description = "공통 코드 관리 API")
@RestController
@RequestMapping("/api/v1/common-codes")
@RequiredArgsConstructor
public class CommonCodeController {
private final CommonCodeService commonCodeService;
@Operation(summary = "공통 코드 목록 조회", description = "그룹 코드에 해당하는 공통 코드 목록을 조회합니다")
@GetMapping
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
public ResponseEntity<ApiResponse<List<CommonCode>>> getCodes(@RequestParam(required = false) String groupCode) {
List<CommonCode> codes = commonCodeService.getCodes(groupCode);
return ResponseEntity.ok(ApiResponse.ok(codes));
}
@Operation(summary = "공통 코드 그룹 목록 조회", description = "모든 공통 코드 그룹 목록을 조회합니다")
@GetMapping("/groups")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
public ResponseEntity<ApiResponse<List<String>>> getGroups() {
List<String> groups = commonCodeService.getGroupCodes();
return ResponseEntity.ok(ApiResponse.ok(groups));
}
@Operation(summary = "공통 코드 생성", description = "새로운 공통 코드를 생성합니다 (ADMIN 전용)")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping
public ResponseEntity<ApiResponse<CommonCode>> createCode(@Valid @RequestBody CommonCodeRequest request) {
CommonCode code = commonCodeService.createCode(request);
return ResponseEntity.ok(ApiResponse.ok("공통 코드가 생성되었습니다.", code));
}
@Operation(summary = "공통 코드 수정", description = "기존 공통 코드를 수정합니다 (ADMIN 전용)")
@PreAuthorize("hasRole('ADMIN')")
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<CommonCode>> updateCode(@PathVariable Long id, @Valid @RequestBody CommonCodeRequest request) {
CommonCode code = commonCodeService.updateCode(id, request);
return ResponseEntity.ok(ApiResponse.ok("공통 코드가 수정되었습니다.", code));
}
@Operation(summary = "공통 코드 삭제", description = "공통 코드를 삭제합니다 (ADMIN 전용)")
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteCode(@PathVariable Long id) {
commonCodeService.deleteCode(id);
return ResponseEntity.ok(ApiResponse.ok("공통 코드가 삭제되었습니다.", null));
}
}
@@ -0,0 +1,82 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.CreateDispatchRequest;
import com.srsystem.entity.DispatchHistory;
import com.srsystem.entity.Employee;
import com.srsystem.service.DispatchHistoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "Dispatch History", description = "파견 이력 관리 API")
@RestController
@RequestMapping("/api/v1/dispatch-histories")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class DispatchHistoryController {
private final DispatchHistoryService dispatchHistoryService;
@Operation(summary = "파견 이력 생성", description = "직원을 프로젝트에 배정합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PostMapping
public ResponseEntity<ApiResponse<DispatchHistory>> createDispatch(
@Valid @RequestBody CreateDispatchRequest request) {
DispatchHistory history = dispatchHistoryService.createDispatch(request);
return ResponseEntity.ok(ApiResponse.ok("파견이 등록되었습니다", history));
}
@Operation(summary = "파견 이력 종료", description = "파견을 종료합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PutMapping("/{dispatchId}/end")
public ResponseEntity<ApiResponse<DispatchHistory>> endDispatch(
@PathVariable Long dispatchId) {
DispatchHistory history = dispatchHistoryService.endDispatch(dispatchId);
return ResponseEntity.ok(ApiResponse.ok("파견이 종료되었습니다", history));
}
@Operation(summary = "현재 파견 중인 직원 목록", description = "파견중인 직원 목록을 조회합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping("/active")
public ResponseEntity<ApiResponse<List<DispatchHistory>>> getActiveDispatches() {
List<DispatchHistory> list = dispatchHistoryService.getActiveDispatches();
return ResponseEntity.ok(ApiResponse.ok(list));
}
@Operation(summary = "파견 이력 전체 목록", description = "페이징 및 필터 조건으로 파견 이력 목록을 조회합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping
public ResponseEntity<ApiResponse<Page<DispatchHistory>>> findAll(
@PageableDefault(size = 20) Pageable pageable,
@RequestParam(required = false) Long empId,
@RequestParam(required = false) Long projectId) {
return ResponseEntity.ok(ApiResponse.ok(dispatchHistoryService.findAll(pageable, empId, projectId)));
}
@Operation(summary = "파견 이력 단건 조회", description = "파견 이력 ID로 단건 조회합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping("/{dispatchId}")
public ResponseEntity<ApiResponse<DispatchHistory>> findById(@PathVariable Long dispatchId) {
return ResponseEntity.ok(ApiResponse.ok(dispatchHistoryService.findById(dispatchId)));
}
@Operation(summary = "파견 이력 삭제", description = "파견 이력을 삭제합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@DeleteMapping("/{dispatchId}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long dispatchId) {
dispatchHistoryService.delete(dispatchId);
return ResponseEntity.ok(ApiResponse.ok("파견 이력이 삭제되었습니다", null));
}
}
@@ -0,0 +1,164 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.ChangePasswordRequest;
import com.srsystem.dto.EmployeeSearchResult;
import com.srsystem.dto.EmployeeSummary;
import com.srsystem.dto.PortfolioResponse;
import com.srsystem.dto.UpdateAchievementsRequest;
import com.srsystem.entity.DispatchHistory;
import com.srsystem.entity.Employee;
import com.srsystem.entity.enums.EmpStatus;
import com.srsystem.service.DispatchHistoryService;
import com.srsystem.service.EmployeeService;
import com.srsystem.service.PortfolioService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.transaction.annotation.Transactional;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
@Tag(name = "Employee", description = "직원 관리 API")
@RestController
@RequestMapping("/api/v1/employees")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class EmployeeController {
private final EmployeeService employeeService;
private final DispatchHistoryService dispatchHistoryService;
private final PortfolioService portfolioService;
@Operation(summary = "직원 목록 조회", description = "페이징된 직원 목록을 조회합니다. 상태 필터 옵션 포함")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping
public ResponseEntity<ApiResponse<Page<Employee>>> findAll(
@RequestParam(required = false) EmpStatus status,
@PageableDefault(size = 20) Pageable pageable) {
Page<Employee> employees = employeeService.findAll(status, pageable);
return ResponseEntity.ok(ApiResponse.ok(employees));
}
@Operation(summary = "직원 단건 조회", description = "직원 ID 로 단일 직원 정보를 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/{empId}")
public ResponseEntity<ApiResponse<Employee>> findById(@PathVariable Long empId) {
Employee employee = employeeService.findById(empId);
return ResponseEntity.ok(ApiResponse.ok(employee));
}
@Operation(summary = "현재 사용자 프로필 조회", description = "로그인한 사용자의 프로필 정보를 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/me")
public ResponseEntity<ApiResponse<EmployeeSummary>> getMe(@CurrentUser Employee employee) {
EmployeeSummary summary = employeeService.toSummary(employee);
return ResponseEntity.ok(ApiResponse.ok(summary));
}
@Operation(summary = "직원 검색 자동완성", description = "이름 키워드로 직원을 검색합니다 (디바운싱 300ms 권장)")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping("/search")
public ResponseEntity<ApiResponse<List<EmployeeSearchResult>>> search(
@RequestParam String keyword) {
List<EmployeeSearchResult> results = employeeService.searchByName(keyword);
return ResponseEntity.ok(ApiResponse.ok(results));
}
@Operation(summary = "직원 생성", description = "새로운 직원 정보를 생성합니다")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping
public ResponseEntity<ApiResponse<Employee>> create(@RequestBody Employee employee) {
Employee created = employeeService.create(employee);
return ResponseEntity.ok(ApiResponse.ok("직원이 등록되었습니다", created));
}
@Operation(summary = "직원 정보 수정", description = "직원 정보를 수정합니다 (퇴사자는 수정 불가)")
@PreAuthorize("hasRole('ADMIN')")
@PutMapping("/{empId}")
public ResponseEntity<ApiResponse<Employee>> update(
@PathVariable Long empId,
@RequestBody Employee employee) {
Employee updated = employeeService.update(empId, employee);
return ResponseEntity.ok(ApiResponse.ok("직원 정보가 수정되었습니다", updated));
}
@Operation(summary = "직원 삭제", description = "직원을 삭제합니다 (Soft Delete, 퇴사자는 삭제 불가)")
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{empId}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long empId) {
employeeService.delete(empId);
return ResponseEntity.ok(ApiResponse.ok("직원이 삭제되었습니다", null));
}
@Operation(summary = "포트폴리오 조회", description = "직원의 파견 이력 (포트폴리오) 을 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@Transactional(readOnly = true)
@GetMapping("/{empId}/dispatch-histories")
public ResponseEntity<ApiResponse<List<DispatchHistory>>> getDispatchHistories(
@PathVariable Long empId,
@Parameter(hidden = true) @CurrentUser Employee currentUser) {
if (!currentUser.getEmpId().equals(empId) &&
!currentUser.getRoleCode().name().equals("ADMIN") &&
!currentUser.getRoleCode().name().equals("MANAGER")) {
return ResponseEntity.status(403).body(ApiResponse.error("권한이 없습니다"));
}
List<DispatchHistory> histories = dispatchHistoryService.findByEmployeeId(empId);
return ResponseEntity.ok(ApiResponse.ok(histories));
}
@Operation(summary = "포트폴리오 조회", description = "직원의 포트폴리오를 조회합니다. 파견 이력별로 보고서 요약과 해결 SR 티켓이 자동 바인딩됩니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/{empId}/portfolio")
public ResponseEntity<ApiResponse<List<PortfolioResponse>>> getPortfolio(
@PathVariable Long empId,
@Parameter(hidden = true) @CurrentUser Employee currentUser) {
if (!currentUser.getEmpId().equals(empId) &&
!currentUser.getRoleCode().name().equals("ADMIN") &&
!currentUser.getRoleCode().name().equals("MANAGER")) {
return ResponseEntity.status(403).body(ApiResponse.error("권한이 없습니다"));
}
List<PortfolioResponse> portfolio = portfolioService.getPortfolio(empId);
return ResponseEntity.ok(ApiResponse.ok(portfolio));
}
@Operation(summary = "포트폴리오 성과 저장", description = "파견 이력의 Achievements 필드를 업데이트합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@PutMapping("/{empId}/dispatch-histories/{dispatchId}/achievements")
public ResponseEntity<ApiResponse<Void>> updateAchievements(
@PathVariable Long empId,
@PathVariable Long dispatchId,
@RequestBody UpdateAchievementsRequest request,
@Parameter(hidden = true) @CurrentUser Employee currentUser) {
if (!currentUser.getEmpId().equals(empId) &&
!currentUser.getRoleCode().name().equals("ADMIN") &&
!currentUser.getRoleCode().name().equals("MANAGER")) {
return ResponseEntity.status(403).body(ApiResponse.error("권한이 없습니다"));
}
portfolioService.updateAchievements(dispatchId, request.getAchievements());
return ResponseEntity.ok(ApiResponse.ok("성과가 저장되었습니다", null));
}
@Operation(summary = "비밀번호 변경", description = "현재 사용자의 비밀번호를 변경합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@PutMapping("/me/password")
public ResponseEntity<ApiResponse<Void>> changePassword(
@RequestBody @Valid ChangePasswordRequest request,
@CurrentUser Employee currentUser) {
employeeService.changePassword(currentUser.getEmpId(), request);
return ResponseEntity.ok(ApiResponse.ok("비밀번호가 변경되었습니다", null));
}
}
@@ -0,0 +1,161 @@
package com.srsystem.controller;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.CreateEquipmentRequest;
import com.srsystem.dto.CreateMaintenanceRequest;
import com.srsystem.dto.EquipmentSummary;
import com.srsystem.dto.UpdateEquipmentRequest;
import com.srsystem.entity.Equipment;
import com.srsystem.entity.EquipmentAssignment;
import com.srsystem.entity.EquipmentMaintenance;
import com.srsystem.entity.enums.EquipmentStatus;
import com.srsystem.entity.enums.EquipmentType;
import com.srsystem.service.EquipmentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
@Tag(name = "Equipment", description = "IT 자산 관리 API")
@RestController
@RequestMapping("/api/v1/equipment")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class EquipmentController {
private final EquipmentService equipmentService;
@Operation(summary = "장비 목록 조회", description = "필터 조건으로 장비 목록을 조회합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping
public ResponseEntity<ApiResponse<Page<Equipment>>> getEquipment(
@RequestParam(required = false) String type,
@RequestParam(required = false) EquipmentStatus status,
@RequestParam(required = false) String keyword,
@PageableDefault(size = 20) Pageable pageable) {
Page<Equipment> equipment = equipmentService.getEquipment(type, status, keyword, pageable);
return ResponseEntity.ok(ApiResponse.ok(equipment));
}
@Operation(summary = "장비 상태별 카운트", description = "장비 상태별 개수를 조회합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping("/summary")
public ResponseEntity<ApiResponse<EquipmentSummary>> getSummary() {
Map<String, Long> counts = equipmentService.getSummary();
EquipmentSummary summary = new EquipmentSummary(counts);
return ResponseEntity.ok(ApiResponse.ok(summary));
}
@Operation(summary = "장비 단건 조회", description = "장비 ID 로 단일 장비 정보를 조회합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping("/{equipId}")
public ResponseEntity<ApiResponse<Equipment>> getEquipment(@PathVariable Long equipId) {
Equipment equipment = equipmentService.getEquipment(equipId);
return ResponseEntity.ok(ApiResponse.ok(equipment));
}
@Operation(summary = "장비 생성", description = "새로운 장비를 생성합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PostMapping
public ResponseEntity<ApiResponse<Equipment>> createEquipment(@Valid @RequestBody CreateEquipmentRequest request) {
Equipment equipment = equipmentService.createEquipment(request);
return ResponseEntity.ok(ApiResponse.ok("장비가 등록되었습니다", equipment));
}
@Operation(summary = "장비 정보 수정", description = "장비 정보를 수정합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PutMapping("/{equipId}")
public ResponseEntity<ApiResponse<Equipment>> updateEquipment(
@PathVariable Long equipId,
@RequestBody UpdateEquipmentRequest request) {
Equipment equipment = equipmentService.updateEquipment(equipId, request);
return ResponseEntity.ok(ApiResponse.ok("장비 정보가 수정되었습니다", equipment));
}
@Operation(summary = "장비 삭제", description = "장비를 삭제합니다 (Soft Delete)")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@DeleteMapping("/{equipId}")
public ResponseEntity<ApiResponse<Void>> deleteEquipment(@PathVariable Long equipId) {
equipmentService.deleteEquipment(equipId);
return ResponseEntity.ok(ApiResponse.ok("장비가 삭제되었습니다", null));
}
@Operation(summary = "장비 지급", description = "사용자에게 장비를 지급합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PostMapping("/{equipId}/assign")
public ResponseEntity<ApiResponse<EquipmentAssignment>> assignEquipment(
@PathVariable Long equipId,
@RequestBody Map<String, Object> body) {
Long userId = ((Number) body.get("userId")).longValue();
String condition = (String) body.get("condition");
EquipmentAssignment assignment = equipmentService.assignEquipment(equipId, userId, condition);
return ResponseEntity.ok(ApiResponse.ok("장비가 지급되었습니다", assignment));
}
@Operation(summary = "장비 반납", description = "장비를 반납합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PutMapping("/{equipId}/return")
public ResponseEntity<ApiResponse<EquipmentAssignment>> returnEquipment(
@PathVariable Long equipId,
@RequestBody Map<String, String> body) {
EquipmentAssignment assignment = equipmentService.returnEquipment(equipId, body.get("condition"));
return ResponseEntity.ok(ApiResponse.ok("장비가 반납되었습니다", assignment));
}
@Operation(summary = "장비 지급 이력 조회", description = "장비의 지급/반납 이력을 조회합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping("/{equipId}/assignments")
public ResponseEntity<ApiResponse<List<EquipmentAssignment>>> getAssignments(@PathVariable Long equipId) {
List<EquipmentAssignment> assignments = equipmentService.getAssignments(equipId);
return ResponseEntity.ok(ApiResponse.ok(assignments));
}
@Operation(summary = "장비 유지보수 이력 조회", description = "장비의 유지보수 이력을 조회합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping("/{equipId}/maintenances")
public ResponseEntity<ApiResponse<List<EquipmentMaintenance>>> getMaintenanceHistory(@PathVariable Long equipId) {
List<EquipmentMaintenance> history = equipmentService.getMaintenanceHistory(equipId);
return ResponseEntity.ok(ApiResponse.ok(history));
}
@Operation(summary = "장비 유지보수 기록 추가", description = "장비의 유지보수 기록을 추가합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PostMapping("/{equipId}/maintenance")
public ResponseEntity<ApiResponse<EquipmentMaintenance>> addMaintenance(
@PathVariable Long equipId,
@Valid @RequestBody CreateMaintenanceRequest request) {
EquipmentMaintenance maintenance = equipmentService.addMaintenance(equipId, request);
return ResponseEntity.ok(ApiResponse.ok("유지보수 기록이 등록되었습니다", maintenance));
}
@Operation(summary = "장비 이미지 추가", description = "장비에 이미지를 추가합니다 (JSONB 배열)")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PostMapping("/{equipmentId}/images")
public ResponseEntity<ApiResponse<Equipment>> addImage(
@PathVariable Long equipmentId,
@RequestParam("file") MultipartFile file) {
Equipment equipment = equipmentService.addImage(equipmentId, file);
return ResponseEntity.ok(ApiResponse.ok("이미지가 추가되었습니다", equipment));
}
@Operation(summary = "장비 이미지 삭제", description = "장비에서 특정 이미지를 삭제합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@DeleteMapping("/{equipmentId}/images/{imageIndex}")
public ResponseEntity<ApiResponse<Equipment>> removeImage(
@PathVariable Long equipmentId,
@PathVariable int imageIndex) {
Equipment equipment = equipmentService.removeImage(equipmentId, imageIndex);
return ResponseEntity.ok(ApiResponse.ok("이미지가 삭제되었습니다", equipment));
}
}
@@ -0,0 +1,61 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.entity.Employee;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@Tag(name = "File", description = "파일 업로드 API")
@RestController
@RequestMapping("/api/v1/files")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class FileUploadController {
@Value("${file.upload-dir:./uploads}")
private String uploadDir;
@PostConstruct
public void init() {
File dir = new File(uploadDir);
if (!dir.exists()) dir.mkdirs();
}
@Operation(summary = "파일 업로드", description = "이미지 파일을 업로드하고 URL 을 반환합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@PostMapping("/upload")
public ResponseEntity<ApiResponse<String>> uploadFile(
@RequestParam("file") MultipartFile file,
@CurrentUser Employee currentUser) {
try {
String originalName = file.getOriginalFilename();
String ext = "";
if (originalName != null && originalName.contains(".")) {
ext = originalName.substring(originalName.lastIndexOf("."));
}
String storedName = UUID.randomUUID().toString() + ext;
Path targetPath = Paths.get(uploadDir, storedName);
Files.copy(file.getInputStream(), targetPath);
String url = "/uploads/" + storedName;
return ResponseEntity.ok(ApiResponse.ok("파일 업로드 성공", url));
} catch (IOException e) {
return ResponseEntity.internalServerError().body(ApiResponse.error("파일 업로드 실패: " + e.getMessage()));
}
}
}
@@ -0,0 +1,51 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.entity.Employee;
import com.srsystem.entity.Notification;
import com.srsystem.service.NotificationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "Notification", description = "알림 관리 API")
@RestController
@RequestMapping("/api/v1/notifications")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class NotificationController {
private final NotificationService notificationService;
@Operation(summary = "내 알림 목록 조회", description = "로그인한 사용자의 알림 목록을 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping
public ResponseEntity<ApiResponse<List<Notification>>> getMyNotifications(@CurrentUser Employee currentUser) {
List<Notification> notifications = notificationService.getMyNotifications(currentUser.getEmpId());
return ResponseEntity.ok(ApiResponse.ok(notifications));
}
@Operation(summary = "안읽은 알림 카운트", description = "안읽은 알림 개수를 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/unread")
public ResponseEntity<ApiResponse<Map<String, Long>>> getUnreadCount(@CurrentUser Employee currentUser) {
long count = notificationService.getUnreadCount(currentUser.getEmpId());
return ResponseEntity.ok(ApiResponse.ok(Map.of("count", count)));
}
@Operation(summary = "알림 읽음 처리", description = "알림을 읽음으로 표시합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@PutMapping("/{notificationId}/read")
public ResponseEntity<ApiResponse<Void>> markAsRead(@PathVariable Long notificationId) {
notificationService.markAsRead(notificationId);
return ResponseEntity.ok(ApiResponse.ok("알림이 읽음 처리되었습니다", null));
}
}
@@ -0,0 +1,39 @@
package com.srsystem.controller;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.HealthResponse;
import com.srsystem.dto.WakeRequest;
import com.srsystem.dto.WakeResponse;
import com.srsystem.service.ParkingService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
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;
@RestController
@RequestMapping("/api/v1")
@Tag(name = "Parking")
public class ParkingController {
private final ParkingService parkingService;
public ParkingController(ParkingService parkingService) {
this.parkingService = parkingService;
}
@GetMapping("/health")
public ResponseEntity<ApiResponse<HealthResponse>> checkHealth() {
HealthResponse response = parkingService.checkStatus();
return ResponseEntity.ok(ApiResponse.ok(response));
}
@PostMapping("/wake")
public ResponseEntity<ApiResponse<WakeResponse>> requestWake(@Valid @RequestBody WakeRequest request) {
WakeResponse response = parkingService.requestWake(request);
return ResponseEntity.ok(ApiResponse.ok("Wake request processed successfully", response));
}
}
@@ -0,0 +1,84 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.CreateProjectRequest;
import com.srsystem.entity.Employee;
import com.srsystem.entity.Project;
import com.srsystem.entity.enums.ProjectStatus;
import com.srsystem.service.ProjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "Project", description = "프로젝트 관리 API")
@RestController
@RequestMapping("/api/v1/projects")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class ProjectController {
private final ProjectService projectService;
@Operation(summary = "내 프로젝트 목록 조회", description = "로그인한 사용자가 참여 중인 프로젝트 목록을 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/my")
public ResponseEntity<ApiResponse<List<Project>>> getMyProjects(@CurrentUser Employee currentUser) {
List<Project> projects = projectService.getMyProjects(currentUser.getEmpId());
return ResponseEntity.ok(ApiResponse.ok(projects));
}
@Operation(summary = "프로젝트 단건 조회", description = "프로젝트 ID 로 단일 프로젝트 정보를 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/{projectId}")
public ResponseEntity<ApiResponse<Project>> getProject(@PathVariable Long projectId) {
Project project = projectService.getProject(projectId);
return ResponseEntity.ok(ApiResponse.ok(project));
}
@Operation(summary = "프로젝트 목록 조회", description = "전체 프로젝트 목록을 조회합니다. 상태 필터 옵션 포함")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@Transactional(readOnly = true)
@GetMapping
public ResponseEntity<ApiResponse<List<Project>>> findAll(
@RequestParam(required = false) ProjectStatus status) {
List<Project> projects = projectService.findAll(status);
return ResponseEntity.ok(ApiResponse.ok(projects));
}
@Operation(summary = "프로젝트 생성", description = "새로운 프로젝트를 등록합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PostMapping
public ResponseEntity<ApiResponse<Project>> create(
@RequestBody @Valid CreateProjectRequest request) {
Project project = projectService.create(request);
return ResponseEntity.ok(ApiResponse.ok("프로젝트가 등록되었습니다", project));
}
@Operation(summary = "프로젝트 수정", description = "프로젝트 정보를 수정합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PutMapping("/{projectId}")
public ResponseEntity<ApiResponse<Project>> update(
@PathVariable Long projectId,
@RequestBody @Valid CreateProjectRequest request) {
Project project = projectService.update(projectId, request);
return ResponseEntity.ok(ApiResponse.ok("프로젝트가 수정되었습니다", project));
}
@Operation(summary = "프로젝트 삭제", description = "프로젝트를 삭제합니다 (Soft Delete)")
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{projectId}")
public ResponseEntity<ApiResponse<Void>> delete(
@PathVariable Long projectId) {
projectService.delete(projectId);
return ResponseEntity.ok(ApiResponse.ok("프로젝트가 삭제되었습니다", null));
}
}
@@ -0,0 +1,129 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.CreateReportRequest;
import com.srsystem.dto.ReportSummary;
import com.srsystem.dto.ReviewReportRequest;
import com.srsystem.dto.TicketSummary;
import com.srsystem.dto.WorkReportResponse;
import com.srsystem.entity.Employee;
import com.srsystem.entity.enums.RoleCode;
import com.srsystem.entity.enums.ReportStatus;
import com.srsystem.entity.enums.ReportType;
import com.srsystem.service.ReportService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@Tag(name = "Report", description = "업무 보고 관리 API")
@RestController
@RequestMapping("/api/v1/reports")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class ReportController {
private final ReportService reportService;
@Operation(summary = "보고서 목록 조회", description = "필터 조건으로 보고서 목록을 조회합니다. EMPLOYEE 는 본인만, MANAGER+ 는 전체")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping
public ResponseEntity<ApiResponse<Page<WorkReportResponse>>> getReports(
@RequestParam(required = false) ReportStatus status,
@RequestParam(required = false) String type,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate targetDate,
@PageableDefault(size = 20) Pageable pageable,
@CurrentUser Employee currentUser) {
Page<WorkReportResponse> reports;
if (currentUser.getRoleCode() == RoleCode.EMPLOYEE) {
// Employee sees only their own reports
reports = reportService.findByUserId(currentUser.getEmpId(), status, type, targetDate, pageable);
} else {
// Manager/Admin see all reports, with SUBMITTED first
reports = reportService.getReports(status, type, targetDate, null, pageable);
}
return ResponseEntity.ok(ApiResponse.ok(reports));
}
@Operation(summary = "보고서 단건 조회", description = "보고서 ID 로 단일 보고서 정보를 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/{reportId}")
public ResponseEntity<ApiResponse<WorkReportResponse>> getReport(@PathVariable Long reportId) {
WorkReportResponse report = reportService.getReport(reportId);
return ResponseEntity.ok(ApiResponse.ok(report));
}
@Operation(summary = "보고서 작성", description = "새로운 업무 보고서를 작성합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@PostMapping
public ResponseEntity<ApiResponse<WorkReportResponse>> createReport(
@Valid @RequestBody CreateReportRequest request,
@CurrentUser Employee currentUser) {
WorkReportResponse report = reportService.createReport(currentUser.getEmpId(), request);
return ResponseEntity.ok(ApiResponse.ok("보고서가 작성되었습니다", report));
}
@Operation(summary = "보고서 상태 변경", description = "보고서 상태를 변경합니다 (DRAFT→SUBMITTED)")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@PutMapping("/{reportId}/status")
public ResponseEntity<ApiResponse<WorkReportResponse>> updateStatus(
@PathVariable Long reportId,
@RequestBody Map<String, String> body,
@CurrentUser Employee currentUser) {
ReportStatus newStatus = ReportStatus.valueOf(body.get("status"));
WorkReportResponse report = reportService.updateStatus(reportId, currentUser.getEmpId(), newStatus);
return ResponseEntity.ok(ApiResponse.ok("보고서 상태가 변경되었습니다", report));
}
@Operation(summary = "보고서 결재", description = "관리자가 보고서를 승인/반려합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PutMapping("/{reportId}/review")
public ResponseEntity<ApiResponse<WorkReportResponse>> reviewReport(
@PathVariable Long reportId,
@Valid @RequestBody ReviewReportRequest request,
@CurrentUser Employee currentUser) {
WorkReportResponse report = reportService.reviewReport(reportId, currentUser.getEmpId(), request);
return ResponseEntity.ok(ApiResponse.ok("보고서가 결재되었습니다", report));
}
@Operation(summary = "보고서 삭제", description = "보고서를 삭제합니다 (LOCKED 상태는 불가)")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@DeleteMapping("/{reportId}")
public ResponseEntity<ApiResponse<Void>> deleteReport(@PathVariable Long reportId) {
reportService.deleteReport(reportId);
return ResponseEntity.ok(ApiResponse.ok("보고서가 삭제되었습니다", null));
}
@Operation(summary = "보고서 결재 요약 조회", description = "상태별 보고서 개수를 반환합니다 (MANAGER/ADMIN)")
@PreAuthorize("hasAnyRole('MANAGER','ADMIN')")
@GetMapping("/summary")
public ResponseEntity<ApiResponse<ReportSummary>> getSummary() {
return ResponseEntity.ok(ApiResponse.ok(reportService.getSummary()));
}
@Operation(summary = "SR 자동 완성", description = "해당 일자의 해결/종료된 SR 내역을 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/sr-auto-complete")
public ResponseEntity<ApiResponse<List<TicketSummary>>> getSrAutoComplete(
@RequestParam String date,
@CurrentUser Employee currentUser) {
List<TicketSummary> tickets = reportService.getResolvedTicketsForDate(currentUser.getEmpId(), date);
return ResponseEntity.ok(ApiResponse.ok(tickets));
}
}
@@ -0,0 +1,131 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.CreateCommentRequest;
import com.srsystem.dto.CreateTicketRequest;
import com.srsystem.dto.IssueTicket;
import com.srsystem.dto.TicketCommentResponse;
import com.srsystem.entity.Employee;
import com.srsystem.entity.enums.RoleCode;
import com.srsystem.entity.enums.TicketCategory;
import com.srsystem.entity.enums.TicketPriority;
import com.srsystem.entity.enums.TicketStatus;
import com.srsystem.service.TicketService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "Ticket", description = "SR 티켓 관리 API")
@RestController
@RequestMapping("/api/v1/tickets")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class TicketController {
private final TicketService ticketService;
@Operation(summary = "티켓 목록 조회", description = "필터 조건으로 티켓 목록을 조회합니다. EMPLOYEE 는 본인 티켓만, MANAGER/ADMIN 은 전체 조회")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping
public ResponseEntity<ApiResponse<Page<IssueTicket>>> getTickets(
@RequestParam(required = false) TicketStatus status,
@RequestParam(required = false) String category,
@RequestParam(required = false) String priority,
@RequestParam(required = false) String keyword,
@PageableDefault(size = 20) Pageable pageable,
@CurrentUser Employee currentUser) {
Long requesterId = currentUser.getRoleCode() == RoleCode.EMPLOYEE ? currentUser.getEmpId() : null;
Page<IssueTicket> tickets = ticketService.getTickets(status, category, priority, keyword, pageable, requesterId);
return ResponseEntity.ok(ApiResponse.ok(tickets));
}
@Operation(summary = "티켓 단건 조회", description = "티켓 ID 로 단일 티켓 정보를 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/{ticketId}")
public ResponseEntity<ApiResponse<IssueTicket>> getTicket(@PathVariable Long ticketId) {
IssueTicket ticket = ticketService.getTicket(ticketId);
return ResponseEntity.ok(ApiResponse.ok(ticket));
}
@Operation(summary = "티켓 생성", description = "새로운 SR 티켓을 생성합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@PostMapping
public ResponseEntity<ApiResponse<IssueTicket>> createTicket(
@Valid @RequestBody CreateTicketRequest request,
@CurrentUser Employee currentUser) {
IssueTicket ticket = ticketService.createTicket(currentUser.getEmpId(), request);
return ResponseEntity.ok(ApiResponse.ok("티켓이 등록되었습니다", ticket));
}
@Operation(summary = "티켓 상태 변경", description = "티켓 상태를 변경합니다 (OPEN→ASSIGNED→IN_PROGRESS→RESOLVED→CLOSED)")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PutMapping("/{ticketId}/status")
public ResponseEntity<ApiResponse<IssueTicket>> updateStatus(
@PathVariable Long ticketId,
@RequestBody Map<String, String> body,
@CurrentUser Employee currentUser) {
TicketStatus newStatus = TicketStatus.valueOf(body.get("status"));
IssueTicket ticket = ticketService.updateStatus(ticketId, currentUser.getEmpId(), newStatus);
return ResponseEntity.ok(ApiResponse.ok("티켓 상태가 변경되었습니다", ticket));
}
@Operation(summary = "티켓 담당자 배정", description = "티켓에 담당자를 배정합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PutMapping("/{ticketId}/assign")
public ResponseEntity<ApiResponse<IssueTicket>> assignTicket(
@PathVariable Long ticketId,
@RequestBody Map<String, Long> body) {
IssueTicket ticket = ticketService.assignTicket(ticketId, body.get("assigneeId"));
return ResponseEntity.ok(ApiResponse.ok("티켓이 배정되었습니다", ticket));
}
@Operation(summary = "티켓 해결 처리", description = "티켓을 해결 처리하고 해결 노트를 기록합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PutMapping("/{ticketId}/resolve")
public ResponseEntity<ApiResponse<IssueTicket>> resolveTicket(
@PathVariable Long ticketId,
@RequestBody Map<String, String> body) {
IssueTicket ticket = ticketService.resolveTicket(ticketId, body.get("resolutionNote"));
return ResponseEntity.ok(ApiResponse.ok("티켓이 해결되었습니다", ticket));
}
@Operation(summary = "티켓 삭제", description = "티켓을 삭제합니다 (Soft Delete)")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@DeleteMapping("/{ticketId}")
public ResponseEntity<ApiResponse<Void>> deleteTicket(@PathVariable Long ticketId) {
ticketService.deleteTicket(ticketId);
return ResponseEntity.ok(ApiResponse.ok("티켓이 삭제되었습니다", null));
}
@Operation(summary = "댓글 목록 조회", description = "티켓의 댓글 목록을 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping("/{ticketId}/comments")
public ResponseEntity<ApiResponse<List<TicketCommentResponse>>> getComments(@PathVariable Long ticketId) {
List<TicketCommentResponse> comments = ticketService.getComments(ticketId);
return ResponseEntity.ok(ApiResponse.ok(comments));
}
@Operation(summary = "댓글 등록", description = "티켓에 댓글을 등록합니다 (CLOSED 티켓은 불가)")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@PostMapping("/{ticketId}/comments")
public ResponseEntity<ApiResponse<TicketCommentResponse>> addComment(
@PathVariable Long ticketId,
@Valid @RequestBody CreateCommentRequest request,
@CurrentUser Employee currentUser) {
TicketCommentResponse response = ticketService.addComment(ticketId, currentUser.getEmpId(), request);
return ResponseEntity.ok(ApiResponse.ok("댓글이 등록되었습니다", response));
}
}
@@ -0,0 +1,55 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.dto.TrainingSummary;
import com.srsystem.dto.UpdateProgressRequest;
import com.srsystem.entity.Employee;
import com.srsystem.entity.TrainingProgress;
import com.srsystem.service.TrainingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "Training", description = "교육 관리 API")
@RestController
@RequestMapping("/api/v1/training")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class TrainingController {
private final TrainingService trainingService;
@Operation(summary = "내 교육 진도 조회", description = "로그인한 사용자의 교육 진도 목록을 조회합니다")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@GetMapping
public ResponseEntity<ApiResponse<List<TrainingProgress>>> getMyProgress(@CurrentUser Employee currentUser) {
List<TrainingProgress> progress = trainingService.getMyProgress(currentUser.getEmpId());
return ResponseEntity.ok(ApiResponse.ok(progress));
}
@Operation(summary = "교육 진도 업데이트", description = "교육 진도율을 업데이트합니다")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@PutMapping("/{progressId}")
public ResponseEntity<ApiResponse<TrainingProgress>> updateProgress(
@PathVariable Long progressId,
@Valid @RequestBody UpdateProgressRequest request) {
TrainingProgress progress = trainingService.updateProgress(progressId, request);
return ResponseEntity.ok(ApiResponse.ok("교육 진도가 업데이트되었습니다", progress));
}
@Operation(summary = "전체 교육 현황 조회", description = "모든 사용자의 교육 이수 현황을 조회합니다 (관리자용)")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
@GetMapping("/summary")
public ResponseEntity<ApiResponse<List<TrainingSummary>>> getSummary() {
List<TrainingSummary> summary = trainingService.getSummary();
return ResponseEntity.ok(ApiResponse.ok(summary));
}
}
@@ -0,0 +1,72 @@
package com.srsystem.controller;
import com.srsystem.config.jwt.CurrentUser;
import com.srsystem.dto.ApiResponse;
import com.srsystem.entity.Employee;
import com.srsystem.entity.WeeklyPlan;
import com.srsystem.service.WeeklyPlanService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "Weekly Plan", description = "주간 계획 관리 API")
@RestController
@RequestMapping("/api/v1/weekly-plans")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
public class WeeklyPlanController {
private final WeeklyPlanService weeklyPlanService;
@GetMapping
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@Operation(summary = "내 주간 계획 목록 조회", description = "현재 사용자의 모든 주간 계획 목록을 조회합니다.")
public ResponseEntity<ApiResponse<List<WeeklyPlan>>> getMyPlans(@CurrentUser Employee currentUser) {
return ResponseEntity.ok(ApiResponse.ok(weeklyPlanService.getMyPlans(currentUser.getEmpId())));
}
@GetMapping("/{planId}")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@Operation(summary = "주간 계획 상세 조회", description = "특정 주간 계획의 상세 정보를 조회합니다.")
public ResponseEntity<ApiResponse<WeeklyPlan>> getPlan(@PathVariable Long planId) {
return ResponseEntity.ok(ApiResponse.ok(weeklyPlanService.getPlan(planId)));
}
@PostMapping
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@Operation(summary = "주간 계획 생성", description = "새로운 주간 계획을 생성합니다.")
public ResponseEntity<ApiResponse<WeeklyPlan>> createPlan(
@RequestBody Map<String, String> body,
@CurrentUser Employee currentUser) {
WeeklyPlan plan = weeklyPlanService.createPlan(currentUser.getEmpId(), body.get("tasks"));
return ResponseEntity.ok(ApiResponse.ok("주간 계획이 생성되었습니다", plan));
}
@PutMapping("/{planId}")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@Operation(summary = "주간 계획 수정", description = "기존 주간 계획의 태스크와 상태를 수정합니다.")
public ResponseEntity<ApiResponse<WeeklyPlan>> updatePlan(
@PathVariable Long planId,
@RequestBody Map<String, String> body) {
String statusStr = body.get("status");
com.srsystem.entity.enums.WeeklyPlanStatus status = statusStr != null ?
com.srsystem.entity.enums.WeeklyPlanStatus.valueOf(statusStr) : null;
WeeklyPlan plan = weeklyPlanService.updatePlan(planId, body.get("tasks"), status);
return ResponseEntity.ok(ApiResponse.ok("주간 계획이 수정되었습니다", plan));
}
@DeleteMapping("/{planId}")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
@Operation(summary = "주간 계획 삭제", description = "주간 계획을 삭제합니다 (Soft Delete).")
public ResponseEntity<ApiResponse<Void>> deletePlan(@PathVariable Long planId) {
weeklyPlanService.deletePlan(planId);
return ResponseEntity.ok(ApiResponse.ok("주간 계획이 삭제되었습니다", null));
}
}
@@ -0,0 +1,16 @@
package com.srsystem.dto;
public record ApiResponse<T>(boolean success, String message, T data) {
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, "OK", data);
}
public static <T> ApiResponse<T> ok(String message, T data) {
return new ApiResponse<>(true, message, data);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null);
}
}
@@ -0,0 +1,10 @@
package com.srsystem.dto;
public record CalendarEvent(
String id,
String title,
String date,
String type,
String status,
String endDate
) {}
@@ -0,0 +1,8 @@
package com.srsystem.dto;
import jakarta.validation.constraints.NotBlank;
public record ChangePasswordRequest(
@NotBlank String currentPassword,
@NotBlank String newPassword
) {}
@@ -0,0 +1,14 @@
package com.srsystem.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record CommonCodeRequest(
@NotBlank String groupCode,
@NotBlank String codeValue,
@NotBlank String codeName,
String optionVal1,
String optionVal2,
@NotNull Integer sortOrder,
@NotNull Character isActive
) {}
@@ -0,0 +1,9 @@
package com.srsystem.dto;
import jakarta.validation.constraints.NotBlank;
public record CreateCommentRequest(
@NotBlank String commentText,
String attachedFileUrl
) {
}
@@ -0,0 +1,16 @@
package com.srsystem.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
public record CreateDispatchRequest(
@NotNull Long employeeId,
@NotNull Long projectId,
@NotBlank String roleTitle,
@NotNull Integer contributionRate,
@NotBlank String individualTech,
@NotBlank String performedTasks,
LocalDate startDate
) {}
@@ -0,0 +1,21 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.EquipmentStatus;
import com.srsystem.entity.enums.EquipmentType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
public record CreateEquipmentRequest(
@NotNull EquipmentType equipmentType,
@NotBlank String manufacturer,
@NotBlank String modelName,
@NotBlank String serialNumber,
@NotBlank String assetCode,
String specifications,
@NotNull EquipmentStatus status,
@NotNull LocalDate purchaseDate,
LocalDate warrantyExpDate
) {
}
@@ -0,0 +1,16 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.MaintenanceType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
public record CreateMaintenanceRequest(
@NotNull MaintenanceType maintenanceType,
@NotBlank String issueDescription,
String actionTaken,
@NotNull BigDecimal cost,
@NotBlank String reportedBy
) {
}
@@ -0,0 +1,44 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.ContractType;
import com.srsystem.entity.enums.ProjectType;
import com.srsystem.entity.enums.WorkLocationType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
public record CreateProjectRequest(
@NotBlank @Size(max = 255)
String projectName,
@NotNull
ProjectType projectType,
@NotBlank @Size(max = 255)
String contractCompany,
@Size(max = 255)
String endUserCompany,
@NotNull
ContractType contractType,
@NotNull
LocalDate startDate,
LocalDate endDate,
@NotNull
WorkLocationType workLocationType,
@Size(max = 500)
String workAddress,
String techEnvironment,
String mainTaskDesc,
Long internalManagerId
) {}
@@ -0,0 +1,23 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.ReportType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
public record CreateReportRequest(
Long dispatchId,
@NotNull ReportType reportType,
@NotNull LocalDate targetDate,
@NotBlank String completedTasks,
@NotBlank String plannedTasks,
String issuesAndRisks,
String supportRequested,
BigDecimal workHours,
String blockerFlag,
String artifacts,
Long weeklyPlanId
) {
}
@@ -0,0 +1,15 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.TicketCategory;
import com.srsystem.entity.enums.TicketPriority;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record CreateTicketRequest(
@NotNull Long projectId,
@NotNull TicketCategory category,
@NotNull TicketPriority priority,
@NotBlank String title,
@NotBlank String description
) {
}
@@ -0,0 +1,9 @@
package com.srsystem.dto;
public record EmployeeSearchResult(
Long empId,
String name,
String email,
String department
) {
}
@@ -0,0 +1,19 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.DispatchStatus;
import com.srsystem.entity.enums.EmpStatus;
import com.srsystem.entity.enums.RoleCode;
import java.time.LocalDate;
public record EmployeeSummary(
Long empId,
String name,
String email,
String phone,
RoleCode roleCode,
EmpStatus empStatus,
DispatchStatus dispatchStatus,
LocalDate joinedDate,
LocalDate leftDate
) {}
@@ -0,0 +1,8 @@
package com.srsystem.dto;
import java.util.Map;
public record EquipmentSummary(
Map<String, Long> counts
) {
}
@@ -0,0 +1,4 @@
package com.srsystem.dto;
public record HealthResponse(boolean parked, String message, String resumeTime, String parkingSchedule) {
}
@@ -0,0 +1,25 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.TicketCategory;
import com.srsystem.entity.enums.TicketPriority;
import com.srsystem.entity.enums.TicketStatus;
import java.time.LocalDateTime;
public record IssueTicket(
Long ticketId,
String projectName,
String requesterName,
String assigneeName,
TicketCategory category,
TicketPriority priority,
String title,
String description,
TicketStatus status,
LocalDateTime dueDate,
LocalDateTime resolvedAt,
String resolutionNote,
boolean slaWarnFlag,
LocalDateTime createdAt
) {
}
@@ -0,0 +1,13 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.DispatchStatus;
import com.srsystem.entity.enums.RoleCode;
import jakarta.validation.constraints.NotBlank;
public record LoginRequest(
@NotBlank(message = "로그인 ID 는 필수입니다")
String loginId,
@NotBlank(message = "비밀번호는 필수입니다")
String password
) {
}
@@ -0,0 +1,11 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.DispatchStatus;
import com.srsystem.entity.enums.RoleCode;
public record LoginResponse(
String accessToken,
String refreshToken,
EmployeeSummary employee
) {
}
@@ -0,0 +1,20 @@
package com.srsystem.dto;
import lombok.Getter;
@Getter
public class NotificationEvent {
private final String type;
private final String title;
private final String message;
private final String linkUrl;
private final Long recipientId;
public NotificationEvent(String type, String title, String message, String linkUrl, Long recipientId) {
this.type = type;
this.title = title;
this.message = message;
this.linkUrl = linkUrl;
this.recipientId = recipientId;
}
}
@@ -0,0 +1,74 @@
package com.srsystem.dto;
import java.time.LocalDate;
import java.util.List;
public class PortfolioResponse {
private Long dispatchId;
private String projectName;
private String roleTitle;
private Integer contributionRate;
private String individualTech;
private String performedTasks;
private String achievements;
private LocalDate startDate;
private LocalDate endDate;
private Integer internalRating;
private List<BoundReport> reports;
private List<BoundTicket> resolvedTickets;
// Inner classes
public static class BoundReport {
public String targetDate;
public String reportType;
public String completedTasks;
}
public static class BoundTicket {
public Long ticketId;
public String title;
public String category;
public String resolutionNote;
}
public PortfolioResponse() {}
// --- Getters & Setters ---
public Long getDispatchId() { return dispatchId; }
public void setDispatchId(Long dispatchId) { this.dispatchId = dispatchId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getRoleTitle() { return roleTitle; }
public void setRoleTitle(String roleTitle) { this.roleTitle = roleTitle; }
public Integer getContributionRate() { return contributionRate; }
public void setContributionRate(Integer contributionRate) { this.contributionRate = contributionRate; }
public String getIndividualTech() { return individualTech; }
public void setIndividualTech(String individualTech) { this.individualTech = individualTech; }
public String getPerformedTasks() { return performedTasks; }
public void setPerformedTasks(String performedTasks) { this.performedTasks = performedTasks; }
public String getAchievements() { return achievements; }
public void setAchievements(String achievements) { this.achievements = achievements; }
public LocalDate getStartDate() { return startDate; }
public void setStartDate(LocalDate startDate) { this.startDate = startDate; }
public LocalDate getEndDate() { return endDate; }
public void setEndDate(LocalDate endDate) { this.endDate = endDate; }
public Integer getInternalRating() { return internalRating; }
public void setInternalRating(Integer internalRating) { this.internalRating = internalRating; }
public List<BoundReport> getReports() { return reports; }
public void setReports(List<BoundReport> reports) { this.reports = reports; }
public List<BoundTicket> getResolvedTickets() { return resolvedTickets; }
public void setResolvedTickets(List<BoundTicket> resolvedTickets) { this.resolvedTickets = resolvedTickets; }
}
@@ -0,0 +1,9 @@
package com.srsystem.dto;
import jakarta.validation.constraints.NotBlank;
public record RefreshTokenRequest(
@NotBlank(message = "리프레시 토큰은 필수입니다")
String refreshToken
) {
}
@@ -0,0 +1,8 @@
package com.srsystem.dto;
public record ReportSummary(
long submitted,
long approved,
long rejected,
long draft
) {}
@@ -0,0 +1,11 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.ReportStatus;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record ReviewReportRequest(
@NotNull ReportStatus status,
String reviewComment
) {
}
@@ -0,0 +1,12 @@
package com.srsystem.dto;
import java.time.LocalDateTime;
public record TicketCommentResponse(
Long commentId,
String userName,
String commentText,
String attachedFileUrl,
LocalDateTime createdAt
) {
}
@@ -0,0 +1,10 @@
package com.srsystem.dto;
public record TicketSummary(
Long ticketId,
String title,
String category,
String priority,
String resolutionNote,
String resolvedAt
) {}
@@ -0,0 +1,10 @@
package com.srsystem.dto;
public record TrainingSummary(
Long userId,
String userName,
long totalCourses,
long completedCount,
double avgProgressRate
) {
}
@@ -0,0 +1,12 @@
package com.srsystem.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UpdateAchievementsRequest {
private String achievements;
}
@@ -0,0 +1,19 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.EquipmentStatus;
import com.srsystem.entity.enums.EquipmentType;
import java.time.LocalDate;
public record UpdateEquipmentRequest(
EquipmentType equipmentType,
String manufacturer,
String modelName,
String serialNumber,
String assetCode,
String specifications,
EquipmentStatus status,
LocalDate purchaseDate,
LocalDate warrantyExpDate
) {
}
@@ -0,0 +1,11 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.TrainingStatus;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
public record UpdateProgressRequest(
@Min(0) @Max(100) Integer progressRate,
TrainingStatus status
) {
}
@@ -0,0 +1,11 @@
package com.srsystem.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record WakeRequest(
@NotBlank String requestedBy,
@NotBlank String reason,
@NotNull String requestedAt
) {
}
@@ -0,0 +1,4 @@
package com.srsystem.dto;
public record WakeResponse(boolean success, String message, String estimatedWakeTime) {
}
@@ -0,0 +1,29 @@
package com.srsystem.dto;
import com.srsystem.entity.enums.ReportStatus;
import com.srsystem.entity.enums.ReportType;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
public record WorkReportResponse(
Long reportId,
String userName,
String projectName,
ReportType reportType,
LocalDate targetDate,
String completedTasks,
String plannedTasks,
String issuesAndRisks,
String supportRequested,
ReportStatus status,
String reviewerName,
String reviewComment,
LocalDateTime reviewedAt,
LocalDateTime createdAt,
BigDecimal workHours,
String blockerFlag,
String artifacts
) {
}
@@ -0,0 +1,38 @@
package com.srsystem.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@SQLDelete(sql = "UPDATE #{#entityName} SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
@Getter
@Setter
public abstract class BaseEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@Column(name = "is_deleted", nullable = false, length = 1)
private char isDeleted = 'N';
public void setIsDeleted(char isDeleted) {
this.isDeleted = isDeleted;
}
}
@@ -0,0 +1,45 @@
package com.srsystem.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
@Entity
@Table(name = "tb_common_code")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_common_code SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class CommonCode extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_common_codes")
@SequenceGenerator(name = "sq_common_codes", sequenceName = "sq_common_codes", allocationSize = 50)
@Column(name = "id")
private Long codeId;
@Column(name = "group_code", nullable = false, length = 50)
private String groupCode;
@Column(name = "code_value", nullable = false, length = 100)
private String codeValue;
@Column(name = "code_name", nullable = false, length = 255)
private String codeName;
@Column(name = "option_val1", length = 255)
private String optionVal1;
@Column(name = "option_val2", length = 255)
private String optionVal2;
@Column(name = "sort_order", nullable = false)
private Integer sortOrder;
@Column(name = "is_active", nullable = false, length = 1)
private char isActive = 'Y';
}
@@ -0,0 +1,43 @@
package com.srsystem.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.math.BigDecimal;
@Entity
@Table(name = "tb_daily_task_progress")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_daily_task_progress SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class DailyTaskProgress extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_daily_task_progress")
@SequenceGenerator(name = "sq_daily_task_progress", sequenceName = "sq_daily_task_progress", allocationSize = 50)
@Column(name = "id")
private Long progressId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "report_id", nullable = false)
private Report report;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "weekly_plan_id", nullable = false)
private WeeklyPlan weeklyPlan;
@Column(name = "task_name", nullable = false, length = 500)
private String taskName;
@Column(name = "progress_rate", nullable = false)
private Integer progressRate;
@Column(name = "work_hours", nullable = false)
private BigDecimal workHours;
}
@@ -0,0 +1,64 @@
package com.srsystem.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.time.LocalDate;
@Entity
@Table(name = "tb_dispatch_history")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_dispatch_history SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class DispatchHistory extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_dispatch_histories")
@SequenceGenerator(name = "sq_dispatch_histories", sequenceName = "sq_dispatch_histories", allocationSize = 50)
@Column(name = "id")
private Long dispatchId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "employee_id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private Employee employee;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "project_id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private Project project;
@Column(name = "start_date", nullable = false)
private LocalDate startDate;
@Column(name = "end_date")
private LocalDate endDate;
@Column(name = "role_title", nullable = false, length = 255)
private String roleTitle;
@Column(name = "contribution_rate", nullable = false)
private Integer contributionRate;
@Column(name = "individual_tech", columnDefinition = "TEXT", nullable = false)
private String individualTech;
@Column(name = "performed_tasks", columnDefinition = "TEXT", nullable = false)
private String performedTasks;
@Column(name = "achievements", columnDefinition = "TEXT")
private String achievements;
@Column(name = "client_feedback", columnDefinition = "TEXT")
private String clientFeedback;
@Column(name = "internal_rating")
private Integer internalRating;
}
@@ -0,0 +1,63 @@
package com.srsystem.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.srsystem.entity.enums.DispatchStatus;
import com.srsystem.entity.enums.EmpStatus;
import com.srsystem.entity.enums.RoleCode;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.time.LocalDate;
@Entity
@Table(name = "tb_employee")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_employee SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class Employee extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_employees")
@SequenceGenerator(name = "sq_employees", sequenceName = "sq_employees", allocationSize = 50)
@Column(name = "id")
private Long empId;
@Column(name = "login_id", nullable = false, unique = true, length = 50)
private String loginId;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "email", length = 255)
private String email;
@Column(name = "phone", length = 20)
private String phone;
@Enumerated(EnumType.STRING)
@Column(name = "role_code", nullable = false, length = 20)
private RoleCode roleCode;
@Enumerated(EnumType.STRING)
@Column(name = "emp_status", nullable = false, length = 20)
private EmpStatus empStatus;
@Enumerated(EnumType.STRING)
@Column(name = "dispatch_status", nullable = false, length = 20)
private DispatchStatus dispatchStatus;
@Column(name = "joined_date")
private LocalDate joinedDate;
@Column(name = "left_date")
private LocalDate leftDate;
}
@@ -0,0 +1,64 @@
package com.srsystem.entity;
import com.srsystem.entity.enums.EquipmentStatus;
import com.srsystem.entity.enums.EquipmentType;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.time.LocalDate;
@Entity
@Table(name = "tb_equipment")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_equipment SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class Equipment extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_equipment")
@SequenceGenerator(name = "sq_equipment", sequenceName = "sq_equipment", allocationSize = 50)
@Column(name = "id")
private Long equipmentId;
@Enumerated(EnumType.STRING)
@Column(name = "equipment_type", nullable = false, length = 20)
private EquipmentType equipmentType;
@Column(name = "manufacturer", nullable = false, length = 255)
private String manufacturer;
@Column(name = "model_name", nullable = false, length = 255)
private String modelName;
@Column(name = "serial_number", nullable = false, unique = true, length = 100)
private String serialNumber;
@Column(name = "asset_code", nullable = false, unique = true, length = 100)
private String assetCode;
@Column(name = "specifications", columnDefinition = "JSONB")
private String specifications;
@Column(name = "images", columnDefinition = "JSONB")
private String images;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private EquipmentStatus status;
@Column(name = "purchase_date", nullable = false)
private LocalDate purchaseDate;
@Column(name = "warranty_exp_date")
private LocalDate warrantyExpDate;
@Version
@Column(name = "version", nullable = false)
private Long version;
}
@@ -0,0 +1,46 @@
package com.srsystem.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.time.LocalDateTime;
@Entity
@Table(name = "tb_equipment_assignment")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_equipment_assignment SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class EquipmentAssignment extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_equipment_assignments")
@SequenceGenerator(name = "sq_equipment_assignments", sequenceName = "sq_equipment_assignments", allocationSize = 50)
@Column(name = "id")
private Long assignmentId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "equipment_id", nullable = false)
private Equipment equipment;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Employee user;
@Column(name = "assigned_date", nullable = false)
private LocalDateTime assignedDate;
@Column(name = "returned_date")
private LocalDateTime returnedDate;
@Column(name = "cond_on_assign", columnDefinition = "TEXT", nullable = false)
private String condOnAssign;
@Column(name = "cond_on_return", columnDefinition = "TEXT")
private String condOnReturn;
}
@@ -0,0 +1,55 @@
package com.srsystem.entity;
import com.srsystem.entity.enums.MaintenanceType;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "tb_equipment_maintenance")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_equipment_maintenance SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class EquipmentMaintenance extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_equipment_maintenance")
@SequenceGenerator(name = "sq_equipment_maintenance", sequenceName = "sq_equipment_maintenance", allocationSize = 50)
@Column(name = "id")
private Long maintenanceId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "equipment_id", nullable = false)
private Equipment equipment;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reported_by", nullable = false)
private Employee reportedBy;
@Enumerated(EnumType.STRING)
@Column(name = "maintenance_type", nullable = false, length = 20)
private MaintenanceType maintenanceType;
@Column(name = "issue_description", columnDefinition = "TEXT", nullable = false)
private String issueDescription;
@Column(name = "action_taken", columnDefinition = "TEXT")
private String actionTaken;
@Column(name = "cost", nullable = false, precision = 12, scale = 2)
private BigDecimal cost;
@Column(name = "reported_date", nullable = false)
private LocalDateTime reportedDate;
@Column(name = "resolved_date")
private LocalDateTime resolvedDate;
}
@@ -0,0 +1,43 @@
package com.srsystem.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
@Entity
@Table(name = "tb_notification")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_notification SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class Notification extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_notifications")
@SequenceGenerator(name = "sq_notifications", sequenceName = "sq_notifications", allocationSize = 50)
@Column(name = "id")
private Long notificationId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Employee user;
@Column(name = "type", nullable = false, length = 50)
private String type;
@Column(name = "title", nullable = false, length = 255)
private String title;
@Column(name = "message", columnDefinition = "TEXT", nullable = false)
private String message;
@Column(name = "is_read", nullable = false, length = 1)
private char isRead = 'N';
@Column(name = "link_url", length = 500)
private String linkUrl;
}
@@ -0,0 +1,76 @@
package com.srsystem.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.srsystem.entity.enums.ContractType;
import com.srsystem.entity.enums.ProjectStatus;
import com.srsystem.entity.enums.ProjectType;
import com.srsystem.entity.enums.WorkLocationType;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.time.LocalDate;
@Entity
@Table(name = "tb_project")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_project SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class Project extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_projects")
@SequenceGenerator(name = "sq_projects", sequenceName = "sq_projects", allocationSize = 50)
@Column(name = "id")
private Long projectId;
@Enumerated(EnumType.STRING)
@Column(name = "project_type", nullable = false, length = 20)
private ProjectType projectType;
@Column(name = "project_name", nullable = false, length = 255)
private String projectName;
@Column(name = "contract_company", nullable = false, length = 255)
private String contractCompany;
@Column(name = "end_user_company", length = 255)
private String endUserCompany;
@Enumerated(EnumType.STRING)
@Column(name = "contract_type", nullable = false, length = 20)
private ContractType contractType;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private ProjectStatus status;
@Column(name = "start_date", nullable = false)
private LocalDate startDate;
@Column(name = "end_date")
private LocalDate endDate;
@Enumerated(EnumType.STRING)
@Column(name = "work_location_type", nullable = false, length = 20)
private WorkLocationType workLocationType;
@Column(name = "work_address", length = 500)
private String workAddress;
@Column(name = "tech_environment", columnDefinition = "TEXT")
private String techEnvironment;
@Column(name = "main_task_desc", columnDefinition = "TEXT")
private String mainTaskDesc;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "internal_manager_id")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private Employee internalManager;
}
@@ -0,0 +1,85 @@
package com.srsystem.entity;
import com.srsystem.entity.enums.ReportStatus;
import com.srsystem.entity.enums.ReportType;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Entity
@Table(name = "tb_report")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_report SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class Report extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_reports")
@SequenceGenerator(name = "sq_reports", sequenceName = "sq_reports", allocationSize = 50)
@Column(name = "id")
private Long reportId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dispatch_id")
private DispatchHistory dispatch;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Employee user;
@Enumerated(EnumType.STRING)
@Column(name = "report_type", nullable = false, length = 20)
private ReportType reportType;
@Column(name = "target_date", nullable = false)
private LocalDate targetDate;
@Column(name = "completed_tasks", columnDefinition = "TEXT", nullable = false)
private String completedTasks;
@Column(name = "planned_tasks", columnDefinition = "TEXT", nullable = false)
private String plannedTasks;
@Column(name = "issues_and_risks", columnDefinition = "TEXT")
private String issuesAndRisks;
@Column(name = "support_requested", columnDefinition = "TEXT")
private String supportRequested;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private ReportStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reviewer_id")
private Employee reviewer;
@Column(name = "review_comment", columnDefinition = "TEXT")
private String reviewComment;
@Column(name = "reviewed_at")
private LocalDateTime reviewedAt;
@Column(name = "work_hours")
private BigDecimal workHours;
@Column(name = "blocker_flag", length = 1)
@Builder.Default
private char blockerFlag = 'N';
@Column(name = "artifacts", columnDefinition = "JSONB")
private String artifacts;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "weekly_plan_id")
private WeeklyPlan weeklyPlan;
}
@@ -0,0 +1,71 @@
package com.srsystem.entity;
import com.srsystem.entity.enums.TicketCategory;
import com.srsystem.entity.enums.TicketPriority;
import com.srsystem.entity.enums.TicketStatus;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.time.LocalDateTime;
@Entity
@Table(name = "tb_ticket")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_ticket SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class Ticket extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_tickets")
@SequenceGenerator(name = "sq_tickets", sequenceName = "sq_tickets", allocationSize = 50)
@Column(name = "id")
private Long ticketId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "project_id", nullable = false)
private Project project;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "requester_id", nullable = false)
private Employee requester;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "assignee_id")
private Employee assignee;
@Enumerated(EnumType.STRING)
@Column(name = "category", nullable = false, length = 20)
private TicketCategory category;
@Enumerated(EnumType.STRING)
@Column(name = "priority", nullable = false, length = 20)
private TicketPriority priority;
@Column(name = "title", nullable = false, length = 500)
private String title;
@Column(name = "description", columnDefinition = "TEXT", nullable = false)
private String description;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private TicketStatus status;
@Column(name = "due_date")
private LocalDateTime dueDate;
@Column(name = "resolved_at")
private LocalDateTime resolvedAt;
@Column(name = "resolution_note", columnDefinition = "TEXT")
private String resolutionNote;
@Column(name = "sla_warn_flag", nullable = false, length = 1)
private char slaWarnFlag = 'N';
}
@@ -0,0 +1,38 @@
package com.srsystem.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
@Entity
@Table(name = "tb_ticket_comment")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_ticket_comment SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class TicketComment extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_ticket_comments")
@SequenceGenerator(name = "sq_ticket_comments", sequenceName = "sq_ticket_comments", allocationSize = 50)
@Column(name = "id")
private Long commentId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ticket_id", nullable = false)
private Ticket ticket;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Employee user;
@Column(name = "comment_text", columnDefinition = "TEXT", nullable = false)
private String commentText;
@Column(name = "attached_file_url", length = 1000)
private String attachedFileUrl;
}
@@ -0,0 +1,39 @@
package com.srsystem.entity;
import com.srsystem.entity.enums.TrainingStatus;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
@Entity
@Table(name = "tb_training_progress")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_training_progress SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class TrainingProgress extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_training_progress")
@SequenceGenerator(name = "sq_training_progress", sequenceName = "sq_training_progress", allocationSize = 50)
@Column(name = "id")
private Long progressId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Employee user;
@Column(name = "course_name", nullable = false, length = 255)
private String courseName;
@Column(name = "progress_rate", nullable = false)
private Integer progressRate;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private TrainingStatus status;
}
@@ -0,0 +1,41 @@
package com.srsystem.entity;
import com.srsystem.entity.enums.WeeklyPlanStatus;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.time.LocalDate;
@Entity
@Table(name = "tb_weekly_plan")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@SQLDelete(sql = "UPDATE tb_weekly_plan SET is_deleted = 'Y' WHERE id = ?")
@SQLRestriction("is_deleted = 'N'")
public class WeeklyPlan extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_weekly_plans")
@SequenceGenerator(name = "sq_weekly_plans", sequenceName = "sq_weekly_plans", allocationSize = 50)
@Column(name = "id")
private Long planId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Employee user;
@Column(name = "week_start", nullable = false)
private LocalDate weekStart;
@Column(name = "status", nullable = false, length = 20)
@Enumerated(EnumType.STRING)
private WeeklyPlanStatus status;
@Column(name = "tasks", columnDefinition = "JSONB")
private String tasks;
}
@@ -0,0 +1,6 @@
package com.srsystem.entity.enums;
public enum ContractType {
TURN_KEY,
MM
}
@@ -0,0 +1,6 @@
package com.srsystem.entity.enums;
public enum DispatchStatus {
WAITING,
DISPATCHED
}
@@ -0,0 +1,7 @@
package com.srsystem.entity.enums;
public enum EmpStatus {
ACTIVE,
ON_LEAVE,
RESIGNED
}
@@ -0,0 +1,9 @@
package com.srsystem.entity.enums;
public enum EquipmentStatus {
AVAILABLE,
IN_USE,
REPAIRING,
RETIRED,
LOST
}
@@ -0,0 +1,8 @@
package com.srsystem.entity.enums;
public enum EquipmentType {
LAPTOP,
MONITOR,
TEST_DEVICE,
ACCESSORY
}
@@ -0,0 +1,8 @@
package com.srsystem.entity.enums;
public enum MaintenanceType {
REPAIR,
UPGRADE,
INSPECTION,
DISPOSAL
}
@@ -0,0 +1,8 @@
package com.srsystem.entity.enums;
public enum ProjectStatus {
PREPARING,
ONGOING,
COMPLETED,
HOLD
}
@@ -0,0 +1,7 @@
package com.srsystem.entity.enums;
public enum ProjectType {
SI,
SM,
MIGRATION
}
@@ -0,0 +1,8 @@
package com.srsystem.entity.enums;
public enum ReportStatus {
DRAFT,
SUBMITTED,
APPROVED,
REJECTED
}
@@ -0,0 +1,8 @@
package com.srsystem.entity.enums;
public enum ReportType {
DAILY,
WEEKLY,
MONTHLY,
RETURN
}
@@ -0,0 +1,7 @@
package com.srsystem.entity.enums;
public enum RoleCode {
ADMIN,
MANAGER,
EMPLOYEE
}
@@ -0,0 +1,8 @@
package com.srsystem.entity.enums;
public enum TicketCategory {
BUG,
FEATURE,
SUPPORT,
QA
}

Some files were not shown because too many files have changed in this diff Show More