EENE Dashboard upload to Gitea

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-17 16:59:34 +09:00
parent cf72281c6d
commit b3f2da203b
138 changed files with 13013 additions and 1929 deletions

153
scripts/db-local.ps1 Normal file
View File

@@ -0,0 +1,153 @@
# Shared config — project DB always lives in data/postgres on port 54320
$script:LocalDbPort = 54320
$script:LocalDbUser = 'eee_admin'
$script:LocalDbPassword = 'eee_password'
$script:LocalDbName = 'eee_dashboard'
function Get-ProjectRoot {
param([string]$FromScript = $PSScriptRoot)
return (Resolve-Path (Join-Path $FromScript '..')).Path
}
function Get-PgDataDir {
param([string]$Root)
return Join-Path $Root 'data\postgres'
}
function Get-PgCtlLogPath {
param([string]$Root)
return Join-Path $Root 'data\pg_ctl.log'
}
function Find-PgBin {
$cmd = Get-Command initdb -ErrorAction SilentlyContinue
if ($cmd) { return (Split-Path $cmd.Path -Parent) }
$candidates = @(
'C:\Program Files\PostgreSQL\16\bin',
'C:\Program Files\PostgreSQL\15\bin',
'C:\Program Files\PostgreSQL\14\bin'
)
foreach ($dir in $candidates) {
if (Test-Path (Join-Path $dir 'initdb.exe')) { return $dir }
}
return $null
}
function Test-DbPortOpen {
param([int]$Port = $script:LocalDbPort)
$client = $null
try {
$client = New-Object System.Net.Sockets.TcpClient
$connect = $client.BeginConnect('127.0.0.1', $Port, $null, $null)
if (-not $connect.AsyncWaitHandle.WaitOne(500, $false)) { return $false }
$client.EndConnect($connect)
return $client.Connected
} catch {
return $false
} finally {
if ($client) { $client.Dispose() }
}
}
function Clear-StalePostmasterLock {
param([string]$PgData)
$pidFile = Join-Path $PgData 'postmaster.pid'
if (-not (Test-Path $pidFile)) { return }
$firstLine = Get-Content $pidFile -TotalCount 1 -ErrorAction SilentlyContinue
$oldPid = 0
[void][int]::TryParse($firstLine, [ref]$oldPid)
$alive = $false
if ($oldPid -gt 0) {
$proc = Get-Process -Id $oldPid -ErrorAction SilentlyContinue
if ($proc -and $proc.ProcessName -match 'postgres') { $alive = $true }
}
if (-not $alive) {
Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
Write-Host ' Cleared stale postmaster.pid (folder moved or crash)'
}
}
function Get-DatabaseUrl {
return "postgresql://$($script:LocalDbUser):$($script:LocalDbPassword)@localhost:$($script:LocalDbPort)/$($script:LocalDbName)"
}
function Set-EnvLine {
param(
[string]$Content,
[string]$Key,
[string]$Value
)
$pattern = "(?m)^$Key="
$line = "$Key=`"$Value`""
if ($Content -match $pattern) {
return ($Content -replace "(?m)^$Key=.*$", $line)
}
return "$line`r`n$Content"
}
function Ensure-BackendEnv {
param([string]$Root)
$envPath = Join-Path $Root 'backend\.env'
$examplePath = Join-Path $Root 'backend\.env.example'
if (-not (Test-Path $envPath)) {
Copy-Item $examplePath $envPath
}
$url = Get-DatabaseUrl
$content = Get-Content $envPath -Raw -Encoding UTF8
$content = Set-EnvLine $content 'DATABASE_URL' $url
$content = Set-EnvLine $content 'HR_DATA_PATH' '../data/seed/hr-data.json'
$content = Set-EnvLine $content 'UPLOAD_DIR' '../uploads'
Set-Content -Path $envPath -Value $content.TrimEnd() -Encoding UTF8 -NoNewline
Add-Content -Path $envPath -Value '' -Encoding UTF8
}
function Set-PostgresPort {
param(
[string]$PgData,
[int]$Port = $script:LocalDbPort
)
$auto = Join-Path $PgData 'postgresql.auto.conf'
$wanted = @{
'port' = "port = $Port"
'logging_collector' = 'logging_collector = off'
}
$existing = @()
if (Test-Path $auto) {
$existing = @(Get-Content $auto -Encoding UTF8 -ErrorAction SilentlyContinue)
}
foreach ($key in $wanted.Keys) {
$line = $wanted[$key]
$found = $false
$existing = @($existing | ForEach-Object {
if ($_ -match "^\s*$key\s*=") { $found = $true; $line } else { $_ }
})
if (-not $found) { $existing += $line }
}
$utf8 = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllLines($auto, $existing, $utf8)
}
function Test-DockerReady {
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { return $false }
docker info 2>$null | Out-Null
return ($LASTEXITCODE -eq 0)
}
function Invoke-Psql {
param(
[string]$PgBin,
[int]$Port = $script:LocalDbPort,
[string]$User = $script:LocalDbUser,
[string]$Database = 'postgres',
[string]$Sql
)
$psql = Join-Path $PgBin 'psql.exe'
& $psql -h 127.0.0.1 -p $Port -U $User -d $Database -v ON_ERROR_STOP=1 -c $Sql 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) { throw "psql failed ($Sql)" }
}

View File

@@ -1,4 +1,49 @@
$ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { $_.IPAddress -match '^(172\.|192\.168\.|10\.)' } |
# 실제 LAN IP (Docker/Hyper-V 172.17.x 등 가상 어댑터 제외)
param(
[string]$PreferredPrefix = '172.16.'
)
$override = $env:EENE_LAN_IP
if ($override) {
Write-Output $override.Trim()
exit 0
}
function Test-PhysicalAdapter {
param([int]$IfIndex)
$adapter = Get-NetAdapter -InterfaceIndex $IfIndex -ErrorAction SilentlyContinue
if (-not $adapter -or $adapter.Status -ne 'Up') { return $false }
$desc = $adapter.InterfaceDescription
if ($desc -match 'Virtual|Hyper-V|Docker|WSL|vEthernet|VMware|Loopback|TAP|TUN|VPN') { return $false }
return $true
}
$candidates = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object {
$_.IPAddress -match '^(172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|10\.)' -and
$_.IPAddress -notmatch '^169\.254\.' -and
$_.IPAddress -notmatch '^172\.17\.' -and
(Test-PhysicalAdapter -IfIndex $_.InterfaceIndex)
}
if (-not $candidates) {
$candidates = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object {
$_.IPAddress -match '^(172\.|192\.168\.|10\.)' -and
$_.IPAddress -notmatch '^169\.254\.' -and
$_.IPAddress -notmatch '^172\.17\.'
}
}
$ip = $candidates |
Sort-Object @{
Expression = {
if ($_.IPAddress -like "$PreferredPrefix*") { 0 }
elseif ($_.IPAddress -match '^192\.168\.') { 1 }
elseif ($_.IPAddress -match '^10\.') { 2 }
else { 3 }
}
}, @{ Expression = { $_.PrefixLength } } |
Select-Object -First 1 -ExpandProperty IPAddress
if ($ip) { Write-Output $ip }

88
scripts/gitea-upload.ps1 Normal file
View File

@@ -0,0 +1,88 @@
# EENE Dashboard -> Gitea upload
# Run: Gitea업로드.bat or powershell -ExecutionPolicy Bypass -File scripts\gitea-upload.ps1
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path $PSScriptRoot -Parent
$pubKeyPath = Join-Path $env:USERPROFILE '.ssh\id_ed25519_eene_dashboard.pub'
Write-Host ''
Write-Host '=== EENE Dashboard Gitea upload ===' -ForegroundColor Cyan
Write-Host ''
if (-not (Test-Path $pubKeyPath)) {
Write-Host '[ERROR] Deploy key not found. Run:' -ForegroundColor Red
Write-Host ' ssh-keygen -t ed25519 -C eene_dashboard-deploy -f $env:USERPROFILE\.ssh\id_ed25519_eene_dashboard -N ""'
exit 1
}
Write-Host '[1/4] Deploy key (paste this line in Gitea -> Settings -> Deploy Keys):' -ForegroundColor Yellow
Write-Host ''
Get-Content $pubKeyPath
Write-Host ''
$ok = Read-Host 'Registered deploy key on Gitea? (y/n)'
if ($ok -ne 'y' -and $ok -ne 'Y') {
Write-Host 'Register the key on Gitea, then run again.' -ForegroundColor Yellow
exit 0
}
Write-Host ''
Write-Host '[2/4] SSH test...' -ForegroundColor Yellow
$keyPath = Join-Path $env:USERPROFILE '.ssh\id_ed25519_eene_dashboard'
$fp = ssh-keygen -lf $pubKeyPath 2>$null
if ($fp) { Write-Host " Key fingerprint: $fp" -ForegroundColor DarkGray }
$giteaPort = 222
$test = ssh -i $keyPath -T git@172.16.10.175 -p $giteaPort -o IdentitiesOnly=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new 2>&1 | Out-String
Write-Host $test.TrimEnd()
if ($test -match 'Permission denied') {
Write-Host ''
Write-Host '[ERROR] Gitea rejected this key.' -ForegroundColor Red
Write-Host " SSH port $giteaPort failed. Gitea SSH is often NOT port 22." -ForegroundColor Yellow
Write-Host ' Check Gitea web -> SSH clone URL for the correct port.' -ForegroundColor Yellow
Write-Host ' Fix on Gitea web:' -ForegroundColor Yellow
Write-Host ' 1. eene_dashboard -> Settings -> Deploy Keys' -ForegroundColor Yellow
Write-Host ' 2. Delete old keys' -ForegroundColor Yellow
Write-Host ' 3. Add key: paste the ssh-ed25519 line from step [1/4] EXACTLY' -ForegroundColor Yellow
Write-Host ' 4. Enable write access, then save' -ForegroundColor Yellow
Write-Host ' Or: Gitea user Settings -> SSH Keys (same public key line)' -ForegroundColor Yellow
exit 1
}
if ($test -notmatch 'successfully authenticated') {
Write-Host ''
Write-Host '[ERROR] Unexpected SSH response. Check port and deploy key.' -ForegroundColor Red
exit 1
}
Write-Host ' SSH OK' -ForegroundColor Green
Set-Location $projectRoot
Write-Host ''
Write-Host '[3/4] git remote gitea...' -ForegroundColor Yellow
$remotes = @(git remote 2>$null)
if ($remotes -contains 'gitea') {
git remote set-url gitea 'git@gitea-eene:RyuWonJun/eene_dashboard.git'
} else {
git remote add gitea 'git@gitea-eene:RyuWonJun/eene_dashboard.git'
}
Write-Host ''
Write-Host '[4/4] commit and push...' -ForegroundColor Yellow
git add -A
$status = git status --porcelain
if ($status) {
git commit -m 'EENE Dashboard upload to Gitea'
} else {
Write-Host ' No changes to commit, push only.' -ForegroundColor DarkGray
}
git push -u gitea main
if ($LASTEXITCODE -ne 0) {
Write-Host ''
Write-Host '[ERROR] push failed. Check branch: git branch' -ForegroundColor Red
exit 1
}
Write-Host ''
Write-Host '=== Done ===' -ForegroundColor Green
Write-Host 'Note: uploads/ and data/postgres/ may not be in Git (.gitignore).' -ForegroundColor Yellow
Write-Host ''

View File

@@ -3,9 +3,9 @@ $killed = @()
Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $ports -contains $_.LocalPort } |
ForEach-Object {
$pid = $_.OwningProcess
if ($killed -notcontains $pid) {
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
$killed += $pid
$procId = $_.OwningProcess
if ($killed -notcontains $procId) {
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
$killed += $procId
}
}

View File

@@ -0,0 +1,111 @@
# One-time: copy data from old Windows PostgreSQL (port 5432) into data/postgres
param(
[string]$Root = '',
[string]$PgBin,
[string]$PgData,
[int]$Port = 0
)
. "$PSScriptRoot\db-local.ps1"
if (-not $Root) { $Root = Get-ProjectRoot -FromScript $PSScriptRoot }
if (-not $Port) { $Port = $LocalDbPort }
$ErrorActionPreference = 'Stop'
function Parse-DatabaseUrl {
param([string]$Url)
if ($Url -match '^postgresql://([^:]+):([^@]+)@([^:/]+):(\d+)/(.+)$') {
return @{
User = $Matches[1]
Password = $Matches[2]
Host = $Matches[3]
Port = [int]$Matches[4]
Database = $Matches[5]
}
}
return $null
}
function Test-LegacyDatabase {
param($Legacy)
if (-not $Legacy) { return $false }
if ($Legacy.Port -eq $Port) { return $false }
if (-not (Test-DbPortOpen -Port $Legacy.Port)) { return $false }
$env:PGPASSWORD = $Legacy.Password
try {
$psql = Join-Path $PgBin 'psql.exe'
$count = & $psql -h $Legacy.Host -p $Legacy.Port -U $Legacy.User -d $Legacy.Database -tAc 'SELECT COUNT(*) FROM tasks' 2>$null
return ($LASTEXITCODE -eq 0 -and [int]$count -gt 0)
} catch {
return $false
} finally {
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
}
}
$envPath = Join-Path $Root 'backend\.env'
$legacy = $null
if (Test-Path $envPath) {
$line = Get-Content $envPath -Encoding UTF8 | Where-Object { $_ -match '^DATABASE_URL=' } | Select-Object -First 1
if ($line -match '^DATABASE_URL="(.+)"') {
$legacy = Parse-DatabaseUrl $Matches[1]
}
}
# Also probe common Windows install (5432 / postgres) when folder is still empty
if (-not (Test-LegacyDatabase -Legacy $legacy)) {
$legacy = @{
User = 'postgres'
Password = $LocalDbPassword
Host = 'localhost'
Port = 5432
Database = $LocalDbName
}
if (-not (Test-LegacyDatabase -Legacy $legacy)) {
return $false
}
}
Write-Host ' [이전] Windows PostgreSQL 데이터를 data\postgres 로 옮기는 중...'
$dumpPath = Join-Path $Root 'data\_legacy_migrate.dump'
$initdb = Join-Path $PgBin 'initdb.exe'
$pgCtl = Join-Path $PgBin 'pg_ctl.exe'
$pgRestore = Join-Path $PgBin 'pg_restore.exe'
$pgDump = Join-Path $PgBin 'pg_dump.exe'
$env:PGPASSWORD = $legacy.Password
& $pgDump -h $legacy.Host -p $legacy.Port -U $legacy.User -d $legacy.Database -F c -f $dumpPath --no-owner --no-acl
if ($LASTEXITCODE -ne 0) {
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
Remove-Item $dumpPath -ErrorAction SilentlyContinue
throw 'legacy pg_dump failed'
}
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
& $initdb -D $PgData -U $LocalDbUser -E UTF8 --locale=C -A trust
if ($LASTEXITCODE -ne 0) { throw 'initdb failed' }
Set-PostgresPort -PgData $PgData -Port $Port
$logFile = Get-PgCtlLogPath -Root $Root
& $pgCtl -D $PgData -l $logFile -o "-p $Port" start -w -t 60
if ($LASTEXITCODE -ne 0) { throw 'pg_ctl start failed (migrate)' }
Start-Sleep -Seconds 2
Invoke-Psql -PgBin $PgBin -Port $Port -Sql "ALTER USER $LocalDbUser WITH PASSWORD '$LocalDbPassword';"
Invoke-Psql -PgBin $PgBin -Port $Port -Sql "CREATE DATABASE $LocalDbName OWNER $LocalDbUser;"
$env:PGPASSWORD = $LocalDbPassword
& $pgRestore -h 127.0.0.1 -p $Port -U $LocalDbUser -d $LocalDbName --no-owner --no-acl $dumpPath 2>&1 | Out-Null
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
if ($LASTEXITCODE -ge 2) {
& $pgCtl -D $PgData stop -m fast
throw 'pg_restore failed'
}
Remove-Item $dumpPath -ErrorAction SilentlyContinue
Ensure-BackendEnv -Root $Root
Write-Host ' [이전] 완료 — 이제 DB는 data\postgres 에 저장됩니다.'
return $true

View File

@@ -0,0 +1,50 @@
param(
[switch]$SkipStop
)
$Root = Split-Path $PSScriptRoot -Parent
Set-Location $Root
Write-Host '================================'
Write-Host ' EENE Dashboard - Data Rebuild'
Write-Host ' Source: data\seed\hr-data.json'
Write-Host '================================'
Write-Host ''
Write-Host ' [WARN] 모든 업무·팀원·첨부 DB 레코드를 지우고'
Write-Host ' hr-data.json 기준으로 다시 만듭니다.'
Write-Host ' (평소 UI 수정은 자동 저장 — 이 파일은 초기화용)'
Write-Host ' uploads\ 파일은 유지됩니다.'
Write-Host ''
$confirm = Read-Host '계속하려면 Y 입력'
if ($confirm -notmatch '^[Yy]') {
Write-Host '취소됨.'
exit 0
}
if (-not $SkipStop) {
Write-Host '[1/3] Stopping API/WEB ...'
& "$PSScriptRoot\stop-server.ps1" | Out-Null
Write-Host ' Done.'
} else {
Write-Host '[1/3] Skip stop (-SkipStop)'
}
Write-Host ''
Write-Host '[2/3] DB sync + rebuild seed ...'
Set-Location (Join-Path $Root 'backend')
cmd /c 'npm run db:sync'
if ($LASTEXITCODE -ne 0) { exit 1 }
cmd /c 'npm run db:seed'
if ($LASTEXITCODE -ne 0) { exit 1 }
Set-Location $Root
Write-Host ' Done.'
Write-Host ''
Write-Host '[3/3] Next steps'
Write-Host ' 1) 서버시작.bat 실행'
Write-Host ' 2) 브라우저 Ctrl+F5'
Write-Host ' 3) (선택) DevTools > Application > Local Storage 에서'
Write-Host ' eene-quarter-hub-config-v1, eene-board-slot-headers-v1 삭제'
Write-Host ''
Write-Host ' Done.'

View File

@@ -0,0 +1,33 @@
import fs from 'fs';
const jsonl =
'C:/Users/한맥/.cursor/projects/d-EENE-Dashboard-0608/agent-transcripts/a31af370-8cc7-4941-8299-96fbb85f1ad8/a31af370-8cc7-4941-8299-96fbb85f1ad8.jsonl';
const cssPath = 'd:/EENE_Dashboard_0608/frontend/src/styles/quarter-board.css';
let content = fs.readFileSync(cssPath, 'utf8');
let applied = 0;
let missed = 0;
for (const line of fs.readFileSync(jsonl, 'utf8').split('\n')) {
if (!line.trim()) continue;
try {
const o = JSON.parse(line);
for (const p of o.message?.content ?? []) {
if (p.type !== 'tool_use') continue;
const inp = p.input ?? {};
if (!inp.path?.includes('quarter-board.css')) continue;
if (!inp.old_string || !inp.new_string) continue;
if (content.includes(inp.old_string)) {
content = content.replace(inp.old_string, inp.new_string);
applied++;
} else {
missed++;
}
}
} catch {
/* skip */
}
}
fs.writeFileSync(cssPath, content, 'utf8');
console.log({ applied, missed, length: content.length });

View File

@@ -0,0 +1,49 @@
import fs from 'fs';
import path from 'path';
const jsonl =
'C:/Users/한맥/.cursor/projects/d-EENE-Dashboard-0608/agent-transcripts/a31af370-8cc7-4941-8299-96fbb85f1ad8/a31af370-8cc7-4941-8299-96fbb85f1ad8.jsonl';
const root = 'd:/EENE_Dashboard_0608';
const files = new Map();
const replaces = [];
for (const line of fs.readFileSync(jsonl, 'utf8').split('\n')) {
if (!line.trim()) continue;
try {
const o = JSON.parse(line);
for (const p of o.message?.content ?? []) {
if (p.type !== 'tool_use') continue;
const inp = p.input ?? {};
const fp = inp.path;
if (!fp?.includes('EENE_Dashboard_0608')) continue;
if (inp.contents) files.set(fp, inp.contents);
if (inp.old_string && inp.new_string && fp.includes('frontend')) {
replaces.push({ fp, old: inp.old_string, new: inp.new_string });
}
}
} catch {
/* skip */
}
}
function toRel(fp) {
return fp
.replace(/\\/g, '/')
.replace(/^d:\/EENE_Dashboard_0608\//i, '')
.replace(/^D:\/EENE_Dashboard_0608\//, '');
}
for (const [fp, initial] of files) {
let content = initial;
for (const r of replaces) {
if (r.fp !== fp) continue;
if (content.includes(r.old)) content = content.replace(r.old, r.new);
}
const rel = toRel(fp);
if (!rel.startsWith('frontend/') && !rel.startsWith('backend/')) continue;
const out = path.join(root, rel);
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, content, 'utf8');
console.log('restored', rel, content.length);
}

129
scripts/start-local-db.ps1 Normal file
View File

@@ -0,0 +1,129 @@
# Start PostgreSQL with data directory = project/data/postgres (no Docker required)
param([string]$Root = '')
. "$PSScriptRoot\db-local.ps1"
function Start-PostgresWithWait {
param(
[string]$PgBin,
[string]$PgData,
[int]$Port,
[int]$TimeoutSec = 120
)
$postgres = Join-Path $PgBin 'postgres.exe'
if (-not (Test-Path $postgres)) {
Write-Host ' [ERROR] postgres.exe not found'
return $false
}
Write-Host ' Launching postgres...'
[Console]::Out.Flush()
# pg_ctl stdout/stderr를 PowerShell 파이프·캡처하면 Windows에서 영구 블로킹될 수 있음 → postgres 직접 기동
Start-Process -FilePath $postgres -ArgumentList @('-D', $PgData, '-p', "$Port") -WindowStyle Hidden | Out-Null
for ($i = 1; $i -le $TimeoutSec; $i++) {
if (Test-DbPortOpen -Port $Port) {
Write-Host " PostgreSQL ready (${i}s)"
return $true
}
if ($i -eq 1 -or $i % 5 -eq 0) {
Write-Host " ... waiting for port $Port ($i/${TimeoutSec}s)"
[Console]::Out.Flush()
}
Start-Sleep -Seconds 1
}
return $false
}
if (-not $Root) { $Root = Get-ProjectRoot -FromScript $PSScriptRoot }
$pgBin = Find-PgBin
if (-not $pgBin) {
Write-Host '[ERROR] PostgreSQL not found. Install PostgreSQL 16 or Docker Desktop.'
return 1
}
$pgData = Get-PgDataDir -Root $Root
New-Item -ItemType Directory -Force -Path $pgData | Out-Null
$initdb = Join-Path $pgBin 'initdb.exe'
$pgCtl = Join-Path $pgBin 'pg_ctl.exe'
$logFile = Get-PgCtlLogPath -Root $Root
$hasData = Test-Path (Join-Path $pgData 'PG_VERSION')
if ($hasData) {
Write-Host ' Existing DB found: data\postgres (keeping your data)'
} else {
Write-Host ' No DB yet - will create data\postgres'
}
Ensure-BackendEnv -Root $Root
Set-PostgresPort -PgData $pgData -Port $LocalDbPort
Clear-StalePostmasterLock -PgData $pgData
if (Test-DbPortOpen -Port $LocalDbPort) {
Write-Host " PostgreSQL already running on port $LocalDbPort"
return 0
}
# pid file only, process dead -> stop cleanly before restart
if (Test-Path (Join-Path $pgData 'postmaster.pid')) {
Write-Host ' Cleaning up previous postgres instance...'
[Console]::Out.Flush()
$stopArgs = @('-D', $pgData, 'stop', '-m', 'fast', '-W', '-t', '5')
$null = Start-Process -FilePath $pgCtl -ArgumentList $stopArgs -WindowStyle Hidden -Wait
Start-Sleep -Seconds 1
Clear-StalePostmasterLock -PgData $pgData
}
if (-not $hasData) {
Write-Host ' Local DB init (data\postgres)...'
$migrated = $false
try {
$migrated = & "$PSScriptRoot\migrate-legacy-db.ps1" -Root $Root -PgBin $pgBin -PgData $pgData -Port $LocalDbPort
} catch {
Write-Warning $_.Exception.Message
if (Test-Path $pgData) {
Get-ChildItem $pgData -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $pgData | Out-Null
}
if (-not $migrated) {
& $initdb -D $pgData -U $LocalDbUser -E UTF8 --locale=C -A trust
if ($LASTEXITCODE -ne 0) {
Write-Host '[ERROR] initdb failed'
return 1
}
Set-PostgresPort -PgData $pgData -Port $LocalDbPort
Clear-StalePostmasterLock -PgData $pgData
Write-Host ' Starting PostgreSQL (new database)...'
if (-not (Start-PostgresWithWait -PgBin $pgBin -PgData $pgData -Port $LocalDbPort)) {
Write-Host '[ERROR] PostgreSQL did not start — see data\pg_ctl.log'
return 1
}
try {
Invoke-Psql -PgBin $pgBin -Port $LocalDbPort -Sql "ALTER USER $LocalDbUser WITH PASSWORD '$LocalDbPassword';"
Invoke-Psql -PgBin $pgBin -Port $LocalDbPort -Sql "CREATE DATABASE $LocalDbName OWNER $LocalDbUser;"
} catch {
Write-Host "[ERROR] $($_.Exception.Message)"
return 1
}
return 0
}
}
Write-Host ' Starting PostgreSQL (recovery after move may take 1-2 min)...'
if (-not (Start-PostgresWithWait -PgBin $pgBin -PgData $pgData -Port $LocalDbPort)) {
Write-Host '[ERROR] PostgreSQL did not start — see data\pg_ctl.log'
Write-Host ' 1) 서버종료.bat db 2) 5초 대기 3) 서버시작.bat'
Write-Host ' PC 종료 전 서버종료.bat db 로 DB 정상 종료 권장'
Write-Host ' OneDrive/백신에서 data\postgres 제외'
return 1
}
return 0

149
scripts/start-server.ps1 Normal file
View File

@@ -0,0 +1,149 @@
$Root = Split-Path $PSScriptRoot -Parent
Set-Location $Root
. "$PSScriptRoot\db-local.ps1"
Write-Host '================================'
Write-Host ' EENE Dashboard - Start'
Write-Host ' Data: data/postgres, uploads/'
Write-Host '================================'
Write-Host ''
foreach ($dir in @('data', 'data\postgres', 'data\seed', 'uploads\team')) {
$p = Join-Path $Root $dir
if (-not (Test-Path $p)) { New-Item -ItemType Directory -Force -Path $p | Out-Null }
}
$envPath = Join-Path $Root 'backend\.env'
$envExample = Join-Path $Root 'backend\.env.example'
if (-not (Test-Path $envPath)) {
Write-Host '[Setup] Creating backend\.env ...'
Copy-Item $envExample $envPath
Write-Host ' Done.'
Write-Host ''
}
Ensure-BackendEnv -Root $Root
Write-Host '[1/4] Stopping existing API/WEB ...'
& "$PSScriptRoot\kill-ports.ps1" | Out-Null
Start-Sleep -Seconds 1
Write-Host ' Done.'
Write-Host ''
Write-Host '[2/4] Starting database (data\postgres) ...'
$dbMode = 'unknown'
if (Test-DockerReady) {
if (Test-Path (Join-Path $Root 'docker-compose.yml')) {
docker compose up -d 2>$null
if ($LASTEXITCODE -eq 0) { $dbMode = 'docker' }
}
}
if ($dbMode -eq 'unknown') {
$dbRc = & "$PSScriptRoot\start-local-db.ps1"
if ($dbRc -eq 0) { $dbMode = 'local' }
}
if ($dbMode -eq 'unknown') {
Write-Host ''
Write-Host ' [ERROR] Cannot start database.'
Write-Host ' - Install PostgreSQL 16 (initdb/pg_ctl in PATH), or'
Write-Host ' - Install Docker Desktop and retry'
Write-Host ''
exit 1
}
Write-Host " DB: $dbMode (port 54320, data\postgres)"
if ($dbMode -eq 'docker') {
$waitRc = & "$PSScriptRoot\wait-db.ps1" -Port 54320
if ($waitRc -ne 0) { exit 1 }
}
Write-Host ''
Write-Host '[3/4] DB schema + seed if empty ...'
Set-Location (Join-Path $Root 'backend')
if (-not (Test-Path 'node_modules')) {
Write-Host ' npm install (backend) ...'
cmd /c 'npm install'
if ($LASTEXITCODE -ne 0) { exit 1 }
}
cmd /c 'npm run db:sync'
if ($LASTEXITCODE -ne 0) {
Write-Host '[ERROR] npm run db:sync failed'
exit 1
}
cmd /c 'npm run db:seed-if-empty'
if ($LASTEXITCODE -ne 0) {
Write-Host '[ERROR] npm run db:seed-if-empty failed'
exit 1
}
cmd /c 'npm run db:seed-team-if-empty'
if ($LASTEXITCODE -ne 0) {
Write-Host '[ERROR] npm run db:seed-team-if-empty failed'
exit 1
}
cmd /c 'npx prisma generate'
if ($LASTEXITCODE -ne 0) {
Write-Host '[ERROR] prisma generate failed (stop other servers and retry)'
exit 1
}
Set-Location $Root
Write-Host ' Done.'
Write-Host ''
Write-Host '[4/4] Starting API + WEB ...'
if (-not (Test-Path (Join-Path $Root 'frontend\node_modules'))) {
Write-Host ' npm install (frontend) ...'
Set-Location (Join-Path $Root 'frontend')
cmd /c 'npm install'
if ($LASTEXITCODE -ne 0) { exit 1 }
Set-Location $Root
}
$lanIp = $null
try {
$ip = & "$PSScriptRoot\get-lan-ip.ps1" 2>$null
if ($ip) { $lanIp = $ip.Trim() }
} catch { }
if (-not $lanIp) {
Write-Host ' [WARN] LAN IP not detected — HTTPS for team may not start.'
Write-Host ' Set EENE_LAN_IP=172.16.8.248 if needed.'
}
$backend = Join-Path $Root 'backend'
$frontend = Join-Path $Root 'frontend'
if ($lanIp) {
Set-Content -Path (Join-Path $frontend '.lan-ip') -Value $lanIp -NoNewline -Encoding ascii
} elseif (Test-Path (Join-Path $frontend '.lan-ip')) {
Remove-Item (Join-Path $frontend '.lan-ip') -Force
}
Write-Host ''
Write-Host ' Local : http://localhost:3000 (듀얼 모니터, 인증서 없음)'
Write-Host ' Admin : http://localhost:3000/admin'
if ($lanIp) {
Write-Host " LAN : https://${lanIp}:3000 (인증서 경고 → 고급 → 계속 → 허용)"
} else {
Write-Host ' LAN : (IP 미감지 — EENE_LAN_IP 환경변수 설정 후 재시작)'
}
Write-Host ' Stop : 서버종료.bat'
Write-Host '================================'
Write-Host ''
$webCmds = @(
"cd /d `"$backend`" && npm run dev:serve",
"cd /d `"$frontend`" && npx --yes wait-on -t 90000 http://127.0.0.1:4000/health && npx vite --mode http-local --host localhost"
)
if ($lanIp) {
$webCmds += "cd /d `"$frontend`" && npx --yes wait-on -t 90000 http://127.0.0.1:4000/health && npx vite --mode https-lan --host $lanIp"
}
$names = if ($lanIp) { 'API,WEB-HTTP,WEB-HTTPS' } else { 'API,WEB-HTTP' }
$colors = if ($lanIp) { 'cyan,green,yellow' } else { 'cyan,green' }
npx --yes concurrently -k -n $names -c $colors $webCmds
Write-Host ''
Write-Host ' Server stopped.'

15
scripts/stop-local-db.ps1 Normal file
View File

@@ -0,0 +1,15 @@
param([string]$Root = '')
. "$PSScriptRoot\db-local.ps1"
if (-not $Root) { $Root = Get-ProjectRoot -FromScript $PSScriptRoot }
$pgBin = Find-PgBin
if (-not $pgBin) { exit 0 }
$pgData = Get-PgDataDir -Root $Root
if (-not (Test-Path (Join-Path $pgData 'PG_VERSION'))) { exit 0 }
$pgCtl = Join-Path $pgBin 'pg_ctl.exe'
& $pgCtl -D $pgData stop -m fast 2>$null
exit 0

26
scripts/stop-server.ps1 Normal file
View File

@@ -0,0 +1,26 @@
param([switch]$Db)
$Root = Split-Path $PSScriptRoot -Parent
Set-Location $Root
Write-Host '================================'
Write-Host ' EENE Dashboard - Stop'
Write-Host '================================'
Write-Host ''
Write-Host '[1/2] Stopping API/WEB ...'
& "$PSScriptRoot\kill-ports.ps1"
Write-Host ' Done.'
Write-Host ''
Write-Host '[2/2] Database ...'
if ($Db) {
& "$PSScriptRoot\stop-local-db.ps1"
docker compose down 2>$null
Write-Host ' DB stopped (data in data\postgres kept)'
} else {
Write-Host ' DB kept running - full stop: 서버종료.bat db'
}
Write-Host ''
Write-Host ' Done.'

18
scripts/wait-api.ps1 Normal file
View File

@@ -0,0 +1,18 @@
param([int]$Port = 4000, [int]$MaxWaitSec = 90)
$url = "http://127.0.0.1:$Port/health"
for ($i = 1; $i -le $MaxWaitSec; $i++) {
try {
$resp = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
if ($resp.StatusCode -eq 200) { return 0 }
} catch { }
if ($i -eq 1) {
Write-Host " Waiting for API on port $Port ..."
}
Start-Sleep -Seconds 1
}
Write-Host "[ERROR] API not ready on port $Port"
return 1

View File

@@ -1,9 +1,11 @@
$ok = $false
1..30 | ForEach-Object {
if (Test-NetConnection -ComputerName localhost -Port 5432 -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Where-Object { $_.TcpTestSucceeded }) {
$ok = $true
break
}
param([int]$Port = 54320)
. "$PSScriptRoot\db-local.ps1"
for ($i = 1; $i -le 30; $i++) {
if (Test-DbPortOpen -Port $Port) { return 0 }
Start-Sleep -Seconds 1
}
if (-not $ok) { exit 1 }
Write-Host "[ERROR] Database port $Port is not ready."
return 1