DB 이름 vexplor → invyone 전환
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m30s

- application.yml, k8s configmap, docker-compose 4종: SPRING_DATASOURCE_URL path
- provisioning 코드: {prefix}_vexplor → {prefix}_invyone 테넌트 DB 네이밍 규칙
- 프론트 마법사: Step1Basic, Step4Run 미리보기 라벨
- CompanyAccordionRow: 기본 DB 이름 포맷
- 마이그레이션/멀티테넌시 문서 동기화
- Traefik 와일드카드 설정 산출물 보관 (notes/)

비밀번호(vexplor0909!!) 및 역사 기록 문서(INVYONE_CONCEPT,
DDD1542, test-output, dashboard-runtime-fixes) 는 의도적으로 미변경.
This commit is contained in:
2026-04-24 19:15:06 +09:00
parent 8c861144dc
commit 76f43cea9b
20 changed files with 116 additions and 37 deletions
@@ -24,7 +24,7 @@ import java.util.UUID;
*
* 6 단계:
* 1. REGISTER_META — COMPANY_MNG 에 db_status='provisioning' 으로 row 선반영 (initiate)
* 2. CREATE_DATABASE — CREATE DATABASE "{prefix}_vexplor"
* 2. CREATE_DATABASE — CREATE DATABASE "{prefix}_invyone"
* 3. COPY_SCHEMA — pg_dump --schema-only | psql
* 4. COPY_DATA — 선택 + 필수 그룹의 테이블들 JDBC 복사
* 5. CREATE_ADMIN — user_info 에 {prefix}_admin INSERT (BCrypt)
@@ -53,7 +53,7 @@ public class CompanyProvisioningService {
private String metaDbName() {
int slash = metaJdbcUrl.lastIndexOf('/');
if (slash < 0) return "vexplor";
if (slash < 0) return "invyone";
String tail = metaJdbcUrl.substring(slash + 1);
int q = tail.indexOf('?');
return q < 0 ? tail : tail.substring(0, q);
@@ -63,7 +63,7 @@ public class CompanyProvisioningService {
public ProvisioningStatus initiate(Map<String, Object> req) {
String companyCode = (String) req.get("company_code");
String dbPrefix = (String) req.get("db_prefix");
String dbName = dbPrefix + "_vexplor";
String dbName = dbPrefix + "_invyone";
String subdomain = (String) req.get("subdomain");
// 템플릿 그룹 수 집계: 필수(3) + 선택된 optional
@@ -99,7 +99,7 @@ public class ProvisioningController {
if (dbPrefix != null) {
boolean formatOk = isValidDbPrefix(dbPrefix);
String dbName = dbPrefix + "_vexplor";
String dbName = dbPrefix + "_invyone";
boolean exists = false;
if (formatOk) {
Map<String, Object> p = new HashMap<>();
@@ -161,7 +161,7 @@ public class ProvisioningController {
"error", "invalid_format", "field", "db_prefix"));
}
// 중복
String dbName = dbPrefix + "_vexplor";
String dbName = dbPrefix + "_invyone";
if (sqlSession.selectOne("provisioning.existsCompanyCode", Map.of("company_code", companyCode)) != null) {
return ResponseEntity.status(409).body(Map.of("error", "duplicate", "field", "company_code"));
}
@@ -17,7 +17,7 @@ public class ProvisioningStatus {
private String id;
/** 생성 요청한 회사 코드 */
private String companyCode;
/** 생성 대상 DB 이름 (예: qnc_vexplor) */
/** 생성 대상 DB 이름 (예: qnc_invyone) */
private String dbName;
/** 서브도메인 */
private String subdomain;
@@ -14,7 +14,7 @@ spring:
jackson:
default-property-inclusion: always
datasource:
url: jdbc:postgresql://183.99.177.40:5432/vexplor
url: jdbc:postgresql://183.99.177.40:5432/invyone
username: postgres
password: "vexplor0909!!"
driver-class-name: org.postgresql.Driver
+1 -1
View File
@@ -11,7 +11,7 @@
SOURCE_HOST="localhost"
SOURCE_PORT="5432"
SOURCE_DB="vexplor"
SOURCE_DB="invyone"
SOURCE_USER="postgres"
TARGET_HOST="대상_호스트"
+4 -4
View File
@@ -14,7 +14,7 @@ Phase 1 은 **라우팅 no-op** 단계 — 컬럼만 추가, 실제 라우팅은
| 컬럼 | 타입 | 설명 |
|---|---|---|
| `DB_NAME` | VARCHAR(64) | 실제 DB 이름 (예: `qnc_vexplor`). Phase 3 이전엔 NULL 허용 |
| `DB_NAME` | VARCHAR(64) | 실제 DB 이름 (예: `qnc_invyone`). Phase 3 이전엔 NULL 허용 |
| `SUBDOMAIN` | VARCHAR(64) | 접속 서브도메인 (예: `qnc`). UNIQUE |
| `DB_HOST` | VARCHAR(128) | DB 서버 호스트 (향후 분산 대비). 기본 NULL |
| `DB_STATUS` | VARCHAR(20) | `provisioning` / `schema_copied` / `admin_created` / `active` / `failed` / `suspended`. 기본 `active` |
@@ -44,13 +44,13 @@ CREATE UNIQUE INDEX IF NOT EXISTS UX_COMPANY_MNG_SUBDOMAIN
## 실행 방법
### 방법 1: DBeaver / pgAdmin
1. `vexplor` DB 연결
1. `invyone` DB 연결
2. 위 SQL 블록 복사 & 실행
3. 확인 쿼리 돌려서 컬럼 생성 여부 확인
### 방법 2: psql
```bash
psql -h 183.99.177.40 -U postgres -d vexplor <<'SQL'
psql -h 183.99.177.40 -U postgres -d invyone <<'SQL'
ALTER TABLE COMPANY_MNG
ADD COLUMN IF NOT EXISTS DB_NAME VARCHAR(64),
ADD COLUMN IF NOT EXISTS SUBDOMAIN VARCHAR(64),
@@ -99,6 +99,6 @@ ALTER TABLE COMPANY_MNG
## 체크리스트
- [ ] `vexplor` DB 에 위 SQL 실행
- [ ] `invyone` DB 에 위 SQL 실행
- [ ] 확인 쿼리 1, 2, 3 모두 통과
- [ ] `SELECT * FROM COMPANY_MNG LIMIT 1;` 로 기존 데이터 정상 조회 확인
+2 -2
View File
@@ -26,11 +26,11 @@ CREATE UNIQUE INDEX IF NOT EXISTS UX_COMPANY_MNG_DB_NAME
## 실행 방법
### DBeaver / pgAdmin
위 SQL 블록을 `vexplor` DB 에 실행.
위 SQL 블록을 `invyone` DB 에 실행.
### psql
```bash
psql -h 183.99.177.40 -U postgres -d vexplor <<'SQL'
psql -h 183.99.177.40 -U postgres -d invyone <<'SQL'
CREATE UNIQUE INDEX IF NOT EXISTS UX_COMPANY_MNG_DB_NAME
ON COMPANY_MNG (DB_NAME)
WHERE DB_NAME IS NOT NULL;
+1 -1
View File
@@ -39,7 +39,7 @@ ALTER TABLE COMPANY_MNG
## 실행
```bash
psql -h 183.99.177.40 -U postgres -d vexplor <<'SQL'
psql -h 183.99.177.40 -U postgres -d invyone <<'SQL'
ALTER TABLE COMPANY_MNG
ADD COLUMN IF NOT EXISTS PLAN VARCHAR(20) DEFAULT 'Starter',
ADD COLUMN IF NOT EXISTS INDUSTRY VARCHAR(50),
+1 -1
View File
@@ -12,7 +12,7 @@ services:
environment:
- SPRING_PROFILES_ACTIVE=dev
- SERVER_PORT=8081
- SPRING_DATASOURCE_URL=jdbc:postgresql://183.99.177.40:5432/vexplor
- SPRING_DATASOURCE_URL=jdbc:postgresql://183.99.177.40:5432/invyone
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=vexplor0909!!
- JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024
+1 -1
View File
@@ -11,7 +11,7 @@ services:
environment:
SPRING_PROFILES_ACTIVE: prod
SERVER_PORT: "8081"
SPRING_DATASOURCE_URL: jdbc:postgresql://183.99.177.40:5432/vexplor
SPRING_DATASOURCE_URL: jdbc:postgresql://183.99.177.40:5432/invyone
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: "vexplor0909!!"
JWT_SECRET: ilshin-plm-super-secret-jwt-key-2024
+1 -1
View File
@@ -12,7 +12,7 @@ services:
environment:
- SPRING_PROFILES_ACTIVE=dev
- SERVER_PORT=8082
- SPRING_DATASOURCE_URL=jdbc:postgresql://183.99.177.40:5432/vexplor
- SPRING_DATASOURCE_URL=jdbc:postgresql://183.99.177.40:5432/invyone
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=vexplor0909!!
- JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024
+1 -1
View File
@@ -12,7 +12,7 @@ services:
environment:
- SPRING_PROFILES_ACTIVE=prod
- SERVER_PORT=8081
- SPRING_DATASOURCE_URL=jdbc:postgresql://183.99.177.40:5432/vexplor
- SPRING_DATASOURCE_URL=jdbc:postgresql://183.99.177.40:5432/invyone
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=vexplor0909!!
- JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024
+9 -9
View File
@@ -11,9 +11,9 @@
INVYONE 은 **회사(=tenant)별로 독립된 PostgreSQL 데이터베이스**를 가진다. 접속한 서브도메인에 따라 요청이 자동으로 해당 회사 DB 로 라우팅된다.
```
qnc.invyone.com → qnc_vexplor DB
kookje.invyone.com → kookje_vexplor DB
vexplor (메타 DB) → 회사 정보·라우팅 테이블·SUPER_ADMIN 데이터
qnc.invyone.com → qnc_invyone DB
kookje.invyone.com → kookje_invyone DB
invyone (메타 DB) → 회사 정보·라우팅 테이블·SUPER_ADMIN 데이터
```
### 설계 원칙
@@ -46,25 +46,25 @@ vexplor (메타 DB) → 회사 정보·라우팅 테이블·SUPER_ADMIN 데이
↓ (HTTP Host 헤더)
[SubdomainResolverFilter]
↓ subdomain=qnc → CompanyResolver.resolveDbName("qnc")
↓ DbContextHolder.set("qnc_vexplor")
↓ DbContextHolder.set("qnc_invyone")
[JwtAuthenticationFilter]
[Controller → Service → sqlSession.selectList(...)]
[TenantRoutingDataSource.determineTargetDataSource()]
↓ DbContextHolder.get() = "qnc_vexplor"
[HikariDataSource for qnc_vexplor] → 실제 SQL 실행
↓ DbContextHolder.get() = "qnc_invyone"
[HikariDataSource for qnc_invyone] → 실제 SQL 실행
```
### 2.3 메타 DB 의 역할
`vexplor` DB (COMPANY_MNG 테이블) 는 **라우팅 룩업 테이블**만 담당:
`invyone` DB (COMPANY_MNG 테이블) 는 **라우팅 룩업 테이블**만 담당:
| 컬럼 | 용도 |
|---|---|
| `COMPANY_CODE` | 회사 식별자 (PK) |
| `SUBDOMAIN` | 서브도메인 prefix (UNIQUE) |
| `DB_NAME` | 실제 tenant DB 이름 (예: `qnc_vexplor`) (UNIQUE) |
| `DB_NAME` | 실제 tenant DB 이름 (예: `qnc_invyone`) (UNIQUE) |
| `DB_HOST` | tenant DB 호스트 (분산 대비, 현재 단일) |
| `DB_STATUS` | `provisioning` / `active` / `failed` / `suspended` |
| `PLAN`, `INDUSTRY`, `TEMPLATES_COUNT`, `DB_QUOTA_GB` | UI 메타 |
@@ -87,7 +87,7 @@ vexplor (메타 DB) → 회사 정보·라우팅 테이블·SUPER_ADMIN 데이
```
1. REGISTER_META COMPANY_MNG 에 status='provisioning' 선반영
2. CREATE_DATABASE CREATE DATABASE "{prefix}_vexplor" (postgres 기본 DB 경유)
2. CREATE_DATABASE CREATE DATABASE "{prefix}_invyone" (postgres 기본 DB 경유)
3. COPY_SCHEMA pg_dump --schema-only | psql (ProcessBuilder 배열 인자)
4. COPY_DATA 선택된 그룹 테이블 JDBC 복사 (company_code 필터)
5. CREATE_ADMIN {prefix}_admin BCrypt 계정 생성
@@ -58,7 +58,7 @@ export default function CompanyAccordionRow({
const sub = r.subdomain || "";
const name = r.company_name || r.name || r.company_code;
const plan = (r.plan || "Starter").toString();
const dbName = r.db_name || `${sub}_vexplor`;
const dbName = r.db_name || `${sub}_invyone`;
const dbPct = Number(r.db_pct) || 0;
const users = Number(r.users) || 0;
const active30 = Number(r.active30) || 0;
@@ -167,7 +167,7 @@ export default function Step1Basic({
value={state.db_prefix || ""}
onChange={onDbPrefixChange}
placeholder="영문 소문자 · 숫자 · 밑줄"
suffix="_vexplor"
suffix="_invyone"
mono
status={availToInputStatus(prefStatus)}
/>
@@ -289,7 +289,7 @@ export default function Step1Basic({
<span style={{ color: "var(--v5-primary)", fontWeight: 700 }}>
{state.db_prefix || "___"}
</span>
<span style={{ color: "var(--v5-text-sec)" }}>_vexplor</span>
<span style={{ color: "var(--v5-text-sec)" }}>_invyone</span>
</PreviewField>
<PreviewField label="회사 코드">
@@ -527,7 +527,7 @@ export default function Step4Run({
idx={i}
status={rowStatus(s.key)}
isLast={i === DISPLAY_STEPS.length - 1}
dbName={`${state.db_prefix || "___"}_vexplor`}
dbName={`${state.db_prefix || "___"}_invyone`}
/>
))}
</div>
+2 -2
View File
@@ -6,13 +6,13 @@ metadata:
data:
SPRING_PROFILES_ACTIVE: "prod"
SERVER_PORT: "8081"
SPRING_DATASOURCE_URL: "jdbc:postgresql://183.99.177.40:5432/vexplor"
SPRING_DATASOURCE_URL: "jdbc:postgresql://183.99.177.40:5432/invyone"
SPRING_DATASOURCE_USERNAME: "postgres"
JWT_EXPIRATION: "86400000"
FILE_UPLOAD_DIR: "./uploads"
NODE_ENV: "production"
NODE_PORT: "8080"
DATABASE_URL: "postgresql://postgres:vexplor0909!!@183.99.177.40:5432/vexplor"
DATABASE_URL: "postgresql://postgres:vexplor0909!!@183.99.177.40:5432/invyone"
CORS_ORIGIN: "https://solution.invyone.com"
NEXT_PUBLIC_API_URL: "https://solution.invyone.com/api"
NEXT_TELEMETRY_DISABLED: "1"
@@ -41,7 +41,7 @@
| 목업에 없지만 UI 에서 파생 | 소스 |
|---|---|
| `{prefix}.invyone.com` 프리뷰 | `SUBDOMAIN` + 하드코딩 도메인 |
| `{prefix}_vexplor` DB 명 | `DB_NAME` |
| `{prefix}_invyone` DB 명 | `DB_NAME` |
| 상태 dot | `DB_STATUS` |
### 1.2 런타임 집계 (CompanyStatsService)
@@ -67,7 +67,7 @@
| 목업 필드 | 계산 |
|---|---|
| `url_preview` (`qnc.invyone.com`) | `${subdomain}.invyone.com` |
| `db_name_display` (`qnc_vexplor`) | `db_name` (이미 있음) |
| `db_name_display` (`qnc_invyone`) | `db_name` (이미 있음) |
---
@@ -86,7 +86,7 @@
"company_code": "COMPANY_10",
"company_name": "큐엔씨",
"subdomain": "qnc",
"db_name": "qnc_vexplor",
"db_name": "qnc_invyone",
"db_host": "183.99.177.40",
"db_status": "active",
"status": "active",
@@ -0,0 +1,44 @@
services:
traefik:
image: traefik:v2.11
container_name: traefik
restart: always
ports:
- "80:80"
- "443:443"
command:
- --api.insecure=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=traefik-net
- --providers.file.directory=/dynamic
- --providers.file.watch=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --entrypoints.web.http.redirections.entryPoint.permanent=true
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
- --certificatesresolvers.le.acme.email=admin@junggomoa.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.ledns.acme.dnschallenge=true
- --certificatesresolvers.ledns.acme.dnschallenge.provider=porkbun
- --certificatesresolvers.ledns.acme.email=admin@junggomoa.com
- --certificatesresolvers.ledns.acme.storage=/letsencrypt/acme-dns.json
- --log.level=INFO
environment:
- PORKBUN_API_KEY=${PORKBUN_API_KEY}
- PORKBUN_SECRET_API_KEY=${PORKBUN_SECRET_API_KEY}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /opt/docker/traefik/letsencrypt:/letsencrypt
- /opt/docker/traefik/dynamic:/dynamic:ro
networks:
- traefik-net
networks:
traefik-net:
external: true
@@ -0,0 +1,35 @@
http:
routers:
invyone-tenant-frontend:
rule: "HostRegexp(`{sub:[a-z0-9-]+}.invyone.com`)"
entryPoints:
- web
- websecure
service: invyone-tenant-frontend
tls:
certResolver: ledns
domains:
- main: invyone.com
sans:
- "*.invyone.com"
invyone-tenant-api:
rule: "HostRegexp(`{sub:[a-z0-9-]+}.invyone.com`) && PathPrefix(`/api`)"
entryPoints:
- web
- websecure
service: invyone-tenant-api
tls:
certResolver: ledns
priority: 100
services:
invyone-tenant-frontend:
loadBalancer:
servers:
- url: "http://host.docker.internal:30000"
invyone-tenant-api:
loadBalancer:
servers:
- url: "http://host.docker.internal:30081"