2026-04-07 10:35:16 +09:00
// 스마트공장 활용 로그 조회 컨트롤러
// 최고관리자(*) 전용 — 회사별 필터링 가능
import { Response } from "express" ;
import { AuthenticatedRequest } from "../middleware/permissionMiddleware" ;
import { query , queryOne } from "../database/db" ;
import { logger } from "../utils/logger" ;
2026-04-07 14:16:26 +09:00
import { encryptionService } from "../services/encryptionService" ;
import {
2026-04-07 16:45:52 +09:00
sendSmartFactoryLog ,
2026-04-07 14:16:26 +09:00
getTodayPlanStatus ,
} from "../utils/smartFactoryLog" ;
2026-04-07 10:35:16 +09:00
/ * *
* GET / api / admin / smart - factory - log
* 스 마 트 공 장 로 그 목 록 조 회
* /
export const getSmartFactoryLogs = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const {
companyCode ,
userId ,
sendStatus ,
dateFrom ,
dateTo ,
search ,
page = "1" ,
limit = "50" ,
} = req . query ;
const whereConditions : string [ ] = [ ] ;
const queryParams : any [ ] = [ ] ;
let paramIndex = 1 ;
// 회사 필터
if ( companyCode && companyCode !== "all" ) {
whereConditions . push ( ` sfl.company_code = $ ${ paramIndex } ` ) ;
queryParams . push ( companyCode ) ;
paramIndex ++ ;
}
// 사용자 필터
if ( userId && ( userId as string ) . trim ( ) ) {
whereConditions . push ( ` sfl.user_id ILIKE $ ${ paramIndex } ` ) ;
queryParams . push ( ` % ${ ( userId as string ) . trim ( ) } % ` ) ;
paramIndex ++ ;
}
// 전송 상태 필터
if ( sendStatus && sendStatus !== "all" ) {
whereConditions . push ( ` sfl.send_status = $ ${ paramIndex } ` ) ;
queryParams . push ( sendStatus ) ;
paramIndex ++ ;
}
// 날짜 범위 필터
if ( dateFrom ) {
whereConditions . push ( ` sfl.created_at >= $ ${ paramIndex } ` ) ;
queryParams . push ( dateFrom ) ;
paramIndex ++ ;
}
if ( dateTo ) {
whereConditions . push ( ` sfl.created_at < ( $ ${ paramIndex } ::date + 1) ` ) ;
queryParams . push ( dateTo ) ;
paramIndex ++ ;
}
// 통합 검색
if ( search && ( search as string ) . trim ( ) ) {
whereConditions . push (
` (sfl.user_id ILIKE $ ${ paramIndex } OR sfl.user_name ILIKE $ ${ paramIndex } OR sfl.connect_ip ILIKE $ ${ paramIndex } OR sfl.error_message ILIKE $ ${ paramIndex } ) `
) ;
queryParams . push ( ` % ${ ( search as string ) . trim ( ) } % ` ) ;
paramIndex ++ ;
}
const whereClause =
whereConditions . length > 0 ? ` WHERE ${ whereConditions . join ( " AND " ) } ` : "" ;
// 총 개수
const countResult = await queryOne < { total : string } > (
` SELECT COUNT(*) as total FROM smart_factory_log sfl ${ whereClause } ` ,
queryParams
) ;
const total = parseInt ( countResult ? . total || "0" , 10 ) ;
// 페이지네이션
const pageNum = Math . max ( 1 , parseInt ( page as string , 10 ) ) ;
const limitNum = Math . min ( 100 , Math . max ( 1 , parseInt ( limit as string , 10 ) ) ) ;
const offset = ( pageNum - 1 ) * limitNum ;
// 데이터 조회 (회사명 JOIN)
const logs = await query < any > (
` SELECT sfl.*, cm.company_name
FROM smart_factory_log sfl
LEFT JOIN company_mng cm ON cm . company_code = sfl . company_code
$ { whereClause }
ORDER BY sfl . created_at DESC
LIMIT $ $ { paramIndex } OFFSET $ $ { paramIndex + 1 } ` ,
[ . . . queryParams , limitNum , offset ]
) ;
res . status ( 200 ) . json ( {
success : true ,
data : logs ,
total ,
page : pageNum ,
limit : limitNum ,
} ) ;
} catch ( error ) {
logger . error ( "스마트공장 로그 조회 실패:" , error ) ;
res . status ( 500 ) . json ( {
success : false ,
message : "스마트공장 로그 조회 중 오류가 발생했습니다." ,
error : {
code : "SERVER_ERROR" ,
details : error instanceof Error ? error . message : "알 수 없는 오류" ,
} ,
} ) ;
}
} ;
/ * *
* GET / api / admin / smart - factory - log / stats
* 스 마 트 공 장 로 그 통 계 ( 회 사 별 요 약 )
* /
export const getSmartFactoryLogStats = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const { companyCode , days = "30" } = req . query ;
const daysNum = parseInt ( days as string , 10 ) || 30 ;
const whereConditions : string [ ] = [
` sfl.created_at >= NOW() - INTERVAL ' ${ daysNum } days' ` ,
] ;
const queryParams : any [ ] = [ ] ;
let paramIndex = 1 ;
if ( companyCode && companyCode !== "all" ) {
whereConditions . push ( ` sfl.company_code = $ ${ paramIndex } ` ) ;
queryParams . push ( companyCode ) ;
paramIndex ++ ;
}
const whereClause = ` WHERE ${ whereConditions . join ( " AND " ) } ` ;
// 상태별 건수
const statusCounts = await query < { send_status : string ; count : string } > (
` SELECT send_status, COUNT(*) as count
FROM smart_factory_log sfl
$ { whereClause }
GROUP BY send_status ` ,
queryParams
) ;
// 회사별 건수
const companyCounts = await query < {
company_code : string ;
company_name : string ;
count : string ;
} > (
` SELECT sfl.company_code, COALESCE(cm.company_name, sfl.company_code) as company_name, COUNT(*) as count
FROM smart_factory_log sfl
LEFT JOIN company_mng cm ON cm . company_code = sfl . company_code
$ { whereClause }
GROUP BY sfl . company_code , cm . company_name
ORDER BY count DESC ` ,
queryParams
) ;
// 일별 추이
const dailyCounts = await query < { date : string ; count : string } > (
` SELECT DATE(sfl.created_at) as date, COUNT(*) as count
FROM smart_factory_log sfl
$ { whereClause }
GROUP BY DATE ( sfl . created_at )
ORDER BY date DESC
LIMIT $ { daysNum } ` ,
queryParams
) ;
// 전체 건수
const totalResult = await queryOne < { total : string } > (
` SELECT COUNT(*) as total FROM smart_factory_log sfl ${ whereClause } ` ,
queryParams
) ;
res . status ( 200 ) . json ( {
success : true ,
data : {
total : parseInt ( totalResult ? . total || "0" , 10 ) ,
statusCounts : statusCounts.map ( ( r ) = > ( {
status : r.send_status ,
count : parseInt ( r . count , 10 ) ,
} ) ) ,
companyCounts : companyCounts.map ( ( r ) = > ( {
companyCode : r.company_code ,
companyName : r.company_name ,
count : parseInt ( r . count , 10 ) ,
} ) ) ,
dailyCounts : dailyCounts.map ( ( r ) = > ( {
date : r.date ,
count : parseInt ( r . count , 10 ) ,
} ) ) ,
} ,
} ) ;
} catch ( error ) {
logger . error ( "스마트공장 로그 통계 조회 실패:" , error ) ;
res . status ( 500 ) . json ( {
success : false ,
message : "통계 조회 중 오류가 발생했습니다." ,
error : {
code : "SERVER_ERROR" ,
details : error instanceof Error ? error . message : "알 수 없는 오류" ,
} ,
} ) ;
}
} ;
2026-04-07 14:16:26 +09:00
// ─── 스케줄 관리 API ───
/ * *
* GET / api / admin / smart - factory - log / schedules
* /
export const getSchedules = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const schedules = await query < any > (
` SELECT s.*, cm.company_name
FROM smart_factory_schedule s
LEFT JOIN company_mng cm ON cm . company_code = s . company_code
ORDER BY s . company_code `
) ;
res . json ( { success : true , data : schedules } ) ;
} catch ( error ) {
logger . error ( "스케줄 조회 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "스케줄 조회 실패" } ) ;
}
} ;
/ * *
* POST / api / admin / smart - factory - log / schedules
* /
export const upsertSchedule = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
2026-04-07 17:29:03 +09:00
const { companyCode , isActive , timeStart , timeEnd , excludeWeekend , excludeHolidays , dailyCount } = req . body ;
2026-04-07 14:16:26 +09:00
if ( ! companyCode ) {
res . status ( 400 ) . json ( { success : false , message : "회사코드는 필수입니다." } ) ;
return ;
}
await query (
2026-04-07 17:29:03 +09:00
` INSERT INTO smart_factory_schedule (company_code, is_active, time_start, time_end, exclude_weekend, exclude_holidays, daily_count, updated_at)
VALUES ( $1 , $2 , $3 , $4 , $5 , $6 , $7 , NOW ( ) )
2026-04-07 14:16:26 +09:00
ON CONFLICT ( company_code ) DO UPDATE SET
is_active = $2 , time_start = $3 , time_end = $4 ,
2026-04-07 17:29:03 +09:00
exclude_weekend = $5 , exclude_holidays = $6 , daily_count = $7 , updated_at = NOW ( ) ` ,
2026-04-07 14:16:26 +09:00
[
companyCode ,
isActive ? ? false ,
timeStart || "08:30" ,
timeEnd || "17:30" ,
excludeWeekend ? ? true ,
excludeHolidays ? ? true ,
2026-04-07 17:29:03 +09:00
Math . max ( 1 , Math . min ( 3 , dailyCount || 1 ) ) ,
2026-04-07 14:16:26 +09:00
]
) ;
2026-04-07 18:21:08 +09:00
// 계획은 매일 00:05에만 생성 (즉시 재생성하면 지난 시각 소급 전송 위험)
res . json ( { success : true , message : "스케줄이 저장되었습니다. 내일 00:05부터 적용됩니다." } ) ;
2026-04-07 14:16:26 +09:00
} catch ( error ) {
logger . error ( "스케줄 저장 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "스케줄 저장 실패" } ) ;
}
} ;
/ * *
* DELETE / api / admin / smart - factory - log / schedules / : companyCode
* /
export const deleteSchedule = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const { companyCode } = req . params ;
await query ( "DELETE FROM smart_factory_schedule WHERE company_code = $1" , [ companyCode ] ) ;
res . json ( { success : true , message : "스케줄이 삭제되었습니다." } ) ;
} catch ( error ) {
logger . error ( "스케줄 삭제 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "스케줄 삭제 실패" } ) ;
}
} ;
/ * *
* POST / api / admin / smart - factory - log / schedules / : companyCode / run - now
* /
/ * *
* GET / api / admin / smart - factory - log / schedules / today - plan
* /
export const getTodayPlanHandler = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const plan = getTodayPlanStatus ( ) ;
res . json ( { success : true , data : plan } ) ;
} catch ( error ) {
logger . error ( "오늘 계획 조회 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "오늘 계획 조회 실패" } ) ;
}
} ;
// ─── 공휴일 관리 API ───
/ * *
* GET / api / admin / smart - factory - log / holidays
* /
export const getHolidays = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const holidays = await query < any > (
"SELECT id, holiday_date, holiday_name, created_at FROM smart_factory_holidays ORDER BY holiday_date"
) ;
res . json ( { success : true , data : holidays } ) ;
} catch ( error ) {
logger . error ( "공휴일 조회 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "공휴일 조회 실패" } ) ;
}
} ;
/ * *
* POST / api / admin / smart - factory - log / holidays
* /
export const addHoliday = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const { holidayDate , holidayName } = req . body ;
if ( ! holidayDate || ! holidayName ) {
res . status ( 400 ) . json ( { success : false , message : "날짜와 이름은 필수입니다." } ) ;
return ;
}
await query (
"INSERT INTO smart_factory_holidays (holiday_date, holiday_name) VALUES ($1, $2) ON CONFLICT (holiday_date) DO UPDATE SET holiday_name = $2" ,
[ holidayDate , holidayName ]
) ;
res . json ( { success : true , message : "공휴일이 추가되었습니다." } ) ;
} catch ( error ) {
logger . error ( "공휴일 추가 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "공휴일 추가 실패" } ) ;
}
} ;
/ * *
* DELETE / api / admin / smart - factory - log / holidays / : id
* /
export const deleteHoliday = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const { id } = req . params ;
await query ( "DELETE FROM smart_factory_holidays WHERE id = $1" , [ id ] ) ;
res . json ( { success : true , message : "공휴일이 삭제되었습니다." } ) ;
} catch ( error ) {
logger . error ( "공휴일 삭제 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "공휴일 삭제 실패" } ) ;
}
} ;
// ─── API 키 관리 ───
/ * *
* GET / api / admin / smart - factory - log / api - keys
* 전 체 회 사 목 록 + API 키 상 태 ( DB키 여 부 , 환 경 변 수 여 부 )
* /
export const getApiKeys = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const companies = await query < any > (
` SELECT cm.company_code, cm.company_name, ak.api_key
FROM company_mng cm
LEFT JOIN smart_factory_api_keys ak ON ak . company_code = cm . company_code
WHERE cm . company_code != '*'
ORDER BY cm . company_code `
) ;
const result = companies . map ( ( c : any ) = > {
let dbKeyDecrypted : string | null = null ;
if ( c . api_key ) {
try {
dbKeyDecrypted = encryptionService . decrypt ( c . api_key ) ;
} catch {
dbKeyDecrypted = "(복호화 실패)" ;
}
}
return {
companyCode : c.company_code ,
companyName : c.company_name ,
hasDbKey : ! ! c . api_key ,
dbKey : dbKeyDecrypted ,
hasEnvKey : ! ! process . env [ ` SMART_FACTORY_API_KEY_ ${ c . company_code } ` ] ,
} ;
} ) ;
res . json ( { success : true , data : result } ) ;
} catch ( error ) {
logger . error ( "API 키 목록 조회 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "API 키 목록 조회 실패" } ) ;
}
} ;
/ * *
* POST / api / admin / smart - factory - log / api - keys
* API 키 저 장 ( 암 호 화 )
* /
export const saveApiKey = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const { companyCode , apiKey } = req . body ;
if ( ! companyCode || ! apiKey ) {
res . status ( 400 ) . json ( { success : false , message : "회사코드와 API 키는 필수입니다." } ) ;
return ;
}
const encrypted = encryptionService . encrypt ( apiKey ) ;
await query (
` INSERT INTO smart_factory_api_keys (company_code, api_key, updated_at)
VALUES ( $1 , $2 , NOW ( ) )
ON CONFLICT ( company_code ) DO UPDATE SET api_key = $2 , updated_at = NOW ( ) ` ,
[ companyCode , encrypted ]
) ;
res . json ( { success : true , message : "API 키가 저장되었습니다." } ) ;
} catch ( error ) {
logger . error ( "API 키 저장 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "API 키 저장 실패" } ) ;
}
} ;
/ * *
* DELETE / api / admin / smart - factory - log / api - keys / : companyCode
* API 키 삭 제 ( 환 경 변 수 폴 백 으 로 전 환 )
* /
export const deleteApiKey = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const { companyCode } = req . params ;
await query (
"DELETE FROM smart_factory_api_keys WHERE company_code = $1" ,
[ companyCode ]
) ;
res . json ( { success : true , message : "API 키가 삭제되었습니다." } ) ;
} catch ( error ) {
logger . error ( "API 키 삭제 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "API 키 삭제 실패" } ) ;
}
} ;
2026-04-07 16:45:52 +09:00
// ─── 즉시 전송 ───
/ * *
* GET / api / admin / smart - factory - log / users / : companyCode
* 회 사 별 사 용 자 목 록 조 회 ( 즉 시 전 송 대 상 선 택 용 )
* /
export const getCompanyUsers = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const { companyCode } = req . params ;
const users = await query < any > (
` SELECT user_id, user_name, dept_name
FROM user_info
WHERE company_code = $1 AND ( status = 'active' OR status IS NULL )
ORDER BY user_name ` ,
[ companyCode ]
) ;
res . json ( { success : true , data : users } ) ;
} catch ( error ) {
logger . error ( "사용자 목록 조회 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "사용자 목록 조회 실패" } ) ;
}
} ;
/ * *
* POST / api / admin / smart - factory - log / send - now
* 선 택 한 사 용 자 즉 시 전 송
* body : { companyCode , userIds : string [ ] , timeStart ? , timeEnd ? }
* /
export const sendNow = async (
req : AuthenticatedRequest ,
res : Response
) : Promise < void > = > {
try {
const { companyCode , userIds } = req . body ;
logger . info ( ` === 즉시 전송 API 호출 === companyCode= ${ companyCode } , userIds= ${ JSON . stringify ( userIds ) } ` ) ;
if ( ! companyCode || ! userIds || userIds . length === 0 ) {
res . status ( 400 ) . json ( { success : false , message : "회사코드와 사용자를 선택해주세요." } ) ;
return ;
}
// 사용자 정보 조회
const users = await query < { user_id : string ; user_name : string } > (
` SELECT user_id, user_name FROM user_info WHERE company_code = $ 1 AND user_id = ANY( $ 2) ` ,
[ companyCode , userIds ]
) ;
logger . info ( ` 즉시 전송 대상: ${ users . length } 명 (조회된 사용자: ${ users . map ( u = > u . user_id ) . join ( ", " ) } ) ` ) ;
// 현재 시간으로 즉시 전송
let success = 0 ;
let fail = 0 ;
const remoteAddr = req . ip || "127.0.0.1" ;
for ( const user of users ) {
try {
logger . info ( ` 즉시 전송 시작: ${ user . user_id } ` ) ;
await sendSmartFactoryLog ( {
userId : user.user_id ,
userName : user.user_name ,
remoteAddr ,
useType : "접속" ,
companyCode ,
} ) ;
success ++ ;
logger . info ( ` 즉시 전송 성공: ${ user . user_id } ` ) ;
} catch ( e ) {
fail ++ ;
logger . error ( ` 즉시 전송 실패: ${ user . user_id } ` , e ) ;
}
}
res . json ( {
success : true ,
data : { total : users.length , success , fail } ,
message : ` ${ success } 명 전송 완료 ${ fail > 0 ? ` , ${ fail } 명 실패 ` : "" } ` ,
} ) ;
} catch ( error ) {
logger . error ( "즉시 전송 실패:" , error ) ;
res . status ( 500 ) . json ( { success : false , message : "즉시 전송 실패" } ) ;
}
} ;