1
0
forked from baron/baron-sso

kratos SSOT 재설계

This commit is contained in:
2026-06-12 18:36:18 +09:00
parent b96c8100e0
commit 8e9d015443
39 changed files with 3960 additions and 501 deletions

232
Makefile
View File

@@ -31,6 +31,10 @@ endif
DUMP_SERVICES ?= all
RESTORE_SERVICES ?= all
FILE_PATH ?=
RESTORE_INPUT ?= $(or $(FILE_PATH),$(word 2,$(MAKECMDGOALS)))
CONFIRM_RESTORE ?=
ALLOW_NON_EMPTY_RESTORE ?= false
DUMP_MODE ?= maintenance
BACKUP_USE_DOCKER ?= true
BACKUP_TOOLS_IMAGE ?= baron-sso-backup-tools:local
@@ -44,54 +48,104 @@ BACKUP_DOCKER_ENV_ARGS += --env-file $(AUTH_CONFIG_ENV)
endif
BACKUP_DOCKER_RUN = docker run --rm $(BACKUP_DOCKER_ENV_ARGS) -e BACKUP_REPO_ROOT=/workspace -v /var/run/docker.sock:/var/run/docker.sock -v "$(CURDIR)":/workspace -v /tmp:/tmp -w /workspace $(BACKUP_TOOLS_IMAGE)
.PHONY: build-auth-config validate-auth-config verify-auth-config render-ory-config up up-all up-infra up-ory up-app up-backend ensure-networks ensure-infra ensure-ory up-dev up-front-dev dev dev-debug down drop down-app down-backend down-infra down-ory check-infra ps logs-infra logs-ory logs-app backup-tools-build dump restore dump-verify restore-verify dump-list restore-plan upload-cloud dump-upload-cloud
.PHONY: help build-auth-config validate-auth-config verify-auth-config render-ory-config up up-all up-infra up-ory up-app up-backend ensure-networks ensure-infra ensure-ory ensure-restore-containers up-dev up-front-dev dev dev-debug down drop down-app down-backend down-infra down-ory check-infra ps logs-infra logs-ory logs-app backup-tools-build dump restore dump-verify restore-verify dump-list restore-plan upload-cloud dump-upload-cloud
help: ## 생성된 타깃과 옵션 목록 표시
@printf "Usage:\n make <target> [OPTION=value ...]\n\n"
@printf "Targets:\n"
@awk ' \
BEGIN { current = ""; printed_section = 0 } \
/^# --- .+ ---/ { \
current = $$0; \
gsub(/^# ---[[:space:]]*/, "", current); \
gsub(/[[:space:]]*---$$/, "", current); \
next; \
} \
/^[[:alnum:]_.-]+:([^=]|$$)/ { \
line = $$0; \
target = line; \
sub(/:.*/, "", target); \
if (target ~ /^\.|%/) { next } \
if (seen[target]++) { next } \
desc = ""; \
if (line ~ /##/) { \
desc = line; \
sub(/^.*##[[:space:]]*/, "", desc); \
} \
if (current != "" && current != printed_section) { \
printf "\n %s\n", current; \
printed_section = current; \
} \
if (desc != "") { \
printf " %-36s %s\n", target, desc; \
} else { \
printf " %-36s\n", target; \
} \
} \
' Makefile
@printf "\nOptions:\n"
@awk ' \
/^[A-Z][A-Z0-9_]+[[:space:]]*\?=/ { \
name = $$1; \
value = $$0; \
sub(/[[:space:]]*\?=.*/, "", name); \
sub(/^[^?]+\?=[[:space:]]*/, "", value); \
printf " %-32s default: %s\n", name, value; \
} \
' Makefile
@printf "\nRestore Safety:\n"
@printf " CONFIRM_RESTORE=baron-sso 복구 실행 의도를 명시하는 필수 확인값\n"
@printf " ALLOW_NON_EMPTY_RESTORE=true 비어 있지 않은 복구 대상에 덮어쓰는 승인된 복구에서만 사용\n"
@printf "\nRestore Examples:\n"
@printf " make restore-plan FILE_PATH=stg.today.tar.gz CONFIRM_RESTORE=baron-sso\n"
@printf " make restore FILE_PATH=stg.today.tar.gz CONFIRM_RESTORE=baron-sso ALLOW_NON_EMPTY_RESTORE=true\n"
# --- 인증 설정 빌드/검증 ---
build-auth-config:
build-auth-config: ## 인증 설정 파일 생성
@echo "Building auth config..."
@mkdir -p config/.generated
@bash scripts/auth_config.sh build
validate-auth-config: build-auth-config
validate-auth-config: build-auth-config ## 인증 설정 값 검증
@echo "Validating auth config..."
@bash scripts/auth_config.sh validate
verify-auth-config: validate-auth-config
verify-auth-config: validate-auth-config ## 인증 설정 연결 상태 확인
@echo "Verifying auth config wiring..."
@bash scripts/auth_config.sh verify
render-ory-config: validate-auth-config
render-ory-config: validate-auth-config ## Ory 설정 파일 렌더링
@echo "Rendering Ory config..."
@bash scripts/render_ory_config.sh
# --- 기본 실행 ---
# 주의: --remove-orphan 사용 금지 (다른 스택이 orphan으로 판단되어 종료될 수 있음)
up: up-all
up: up-all ## 전체 로컬 스택 실행
up-all: ensure-networks render-ory-config
up-all: ensure-networks render-ory-config ## 인프라, Ory, 앱 스택 모두 실행
@echo "Starting ALL stacks (infra + ory + app)..."
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up --build -d
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) restart kratos
# --- 개별 스택 실행 ---
up-infra: ensure-networks
up-infra: ensure-networks ## 인프라 스택 실행
@echo "Starting Infra stack (postgres/clickhouse/redis)..."
docker compose -f $(COMPOSE_INFRA) up -d
up-ory: ensure-networks render-ory-config
up-ory: ensure-networks render-ory-config ## Ory 스택 실행
@echo "Starting Ory stack (kratos/hydra/keto/oathkeeper)..."
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) restart kratos
up-app: ensure-networks render-ory-config
up-app: ensure-networks render-ory-config ## 앱 스택 실행
@echo "Starting App stack (backend/userfront/adminfront/devfront/orgfront)..."
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build -d
up-backend: ensure-networks render-ory-config
up-backend: ensure-networks render-ory-config ## 백엔드 컨테이너만 실행
@echo "Starting Backend only..."
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build -d backend
ensure-networks:
ensure-networks: ## 개발용 Docker 네트워크 보장
@echo "Ensuring Docker networks..."
@for network in $(DEV_NETWORKS); do \
if ! docker network inspect "$$network" >/dev/null 2>&1; then \
@@ -102,7 +156,7 @@ ensure-networks:
fi; \
done
ensure-infra: ensure-networks
ensure-infra: ensure-networks ## 인프라 스택 실행 상태 보장
@echo "Ensuring Infra stack..."
@missing=0; \
for container in $(INFRA_CONTAINERS); do \
@@ -118,7 +172,7 @@ ensure-infra: ensure-networks
echo "Infra stack is already running."; \
fi
ensure-ory: ensure-networks render-ory-config
ensure-ory: ensure-networks render-ory-config ## Ory 스택 실행 상태 보장
@echo "Ensuring Ory stack..."
@missing=0; \
for container in $(ORY_CONTAINERS); do \
@@ -135,26 +189,74 @@ ensure-ory: ensure-networks render-ory-config
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) restart kratos; \
fi
up-dev: ensure-infra ensure-ory
ensure-restore-containers: ## 복구 대상 저장소 컨테이너 실행 상태 보장
@echo "Ensuring restore target containers..."
@if [ "$(CONFIRM_RESTORE)" != "baron-sso" ]; then \
echo "Skipping restore target container startup until CONFIRM_RESTORE=baron-sso is provided."; \
exit 0; \
fi
@$(MAKE) --no-print-directory ensure-networks
@ensure_restore_container() { \
container="$$1"; \
compose_file="$$2"; \
compose_service="$$3"; \
if docker inspect -f '{{.State.Running}}' "$$container" 2>/dev/null | grep -qx 'true'; then \
echo "Restore target container $$container is already running."; \
return 0; \
fi; \
if docker inspect "$$container" >/dev/null 2>&1; then \
echo "Starting stopped restore target container $$container..."; \
docker start "$$container"; \
else \
echo "Creating restore target container $$container via $$compose_file service $$compose_service..."; \
docker compose -f "$$compose_file" up -d "$$compose_service"; \
fi; \
for attempt in 1 2 3 4 5 6 7 8 9 10; do \
if docker inspect -f '{{.State.Running}}' "$$container" 2>/dev/null | grep -qx 'true'; then \
return 0; \
fi; \
sleep 1; \
done; \
echo "ERROR: restore target container $$container did not reach running state." >&2; \
return 1; \
}; \
services="$(RESTORE_SERVICES)"; \
if [ -z "$$services" ] || [ "$$services" = "all" ]; then \
services="postgres ory-postgres clickhouse ory-clickhouse config"; \
else \
services="$$(printf '%s' "$$services" | tr ',' ' ')"; \
fi; \
for service in $$services; do \
case "$$service" in \
postgres) ensure_restore_container baron_postgres compose.infra.yaml postgres ;; \
ory-postgres) ensure_restore_container ory_postgres compose.ory.yaml postgres ;; \
clickhouse) ensure_restore_container baron_clickhouse compose.infra.yaml clickhouse ;; \
ory-clickhouse) ensure_restore_container ory_clickhouse compose.ory.yaml ory_clickhouse ;; \
config) ;; \
*) echo "ERROR: unknown restore service: $$service" >&2; exit 1 ;; \
esac; \
done
up-dev: ensure-infra ensure-ory ## 개발 기본 스택 준비
@echo "Dev stack is up (infra + ory)."
up-front-dev: up-infra up-ory up-backend
up-front-dev: up-infra up-ory up-backend ## 프론트 개발용 의존 스택 준비
@echo "Dev stack is up (infra + ory + backend)."
dev: up-dev
dev: up-dev ## 개발 앱 컨테이너를 포그라운드로 실행
@echo "Starting development app containers in foreground attach mode..."
BACKEND_LOG_LEVEL=info CLIENT_LOG_DEBUG=false VITE_CLIENT_LOG_DEBUG=false docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build $(DEV_SERVICES)
dev-debug: up-dev
dev-debug: up-dev ## 디버그 로그로 개발 앱 컨테이너 실행
@echo "Starting development app containers in foreground attach debug mode..."
BACKEND_LOG_LEVEL=debug CLIENT_LOG_DEBUG=true VITE_CLIENT_LOG_DEBUG=true USERFRONT_FLUTTER_RUN_FLAGS=--debug docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build $(DEV_SERVICES)
# --- 종료 (Down) ---
down:
down: ## 전체 로컬 스택 중지
@echo "Stopping ALL stacks (infra + ory + app)..."
docker compose -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) down
drop:
drop: ## 로컬 스택 컨테이너, 볼륨, 로컬 이미지 제거
@echo "Dropping Baron SSO local Docker stack containers, volumes, and local images..."
-docker compose $(COMPOSE_DROP_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) down -v --rmi local
@echo "Removing any remaining fixed-name Baron SSO containers..."
@@ -163,25 +265,25 @@ drop:
done
@echo "Drop complete. External Docker networks are preserved."
down-app:
down-app: ## 앱 스택 중지
@echo "Stopping App stack..."
docker compose -f $(COMPOSE_APP) down
down-backend:
down-backend: ## 백엔드 컨테이너 중지
@echo "Stopping Backend only..."
docker compose -f $(COMPOSE_APP) stop backend
down-infra:
down-infra: ## 인프라 스택 중지
@echo "Stopping Infra stack..."
docker compose -f $(COMPOSE_INFRA) down
down-ory:
down-ory: ## Ory 스택 중지
@echo "Stopping Ory stack..."
docker compose -f $(COMPOSE_ORY) down
# --- 유틸리티 ---
# 인프라 상태 확인
check-infra:
check-infra: ## 인프라 헬스 상태 확인
@echo "Checking infra status..."
@if [ "$$(docker inspect -f '{{.State.Health.Status}}' baron_postgres 2>/dev/null)" != "healthy" ]; then \
echo "Error: PostgreSQL is not running or not healthy."; \
@@ -191,67 +293,67 @@ check-infra:
echo "PostgreSQL is healthy."; \
fi
ps:
ps: ## 전체 Compose 컨테이너 상태 조회
docker compose -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) ps
logs-infra:
logs-infra: ## 인프라 스택 로그 팔로우
docker compose -f $(COMPOSE_INFRA) logs -f
logs-ory:
logs-ory: ## Ory 스택 로그 팔로우
docker compose -f $(COMPOSE_ORY) logs -f
logs-app:
logs-app: ## 앱 스택 로그 팔로우
docker compose -f $(COMPOSE_APP) logs -f
# --- 백업/복구 ---
backup-tools-build:
backup-tools-build: ## 백업 도구 Docker 이미지 빌드
docker build -f $(BACKUP_TOOLS_DOCKERFILE) -t $(BACKUP_TOOLS_IMAGE) .
ifeq ($(BACKUP_USE_DOCKER),true)
dump: backup-tools-build
dump: backup-tools-build ## 백업 덤프 생성
$(BACKUP_DOCKER_RUN) bash -lc 'DUMP_SERVICES="$(DUMP_SERVICES)" DUMP_MODE="$(DUMP_MODE)" BACKUP="$(BACKUP)" BACKUP_ROOT="$(BACKUP_ROOT)" scripts/backup/dump.sh'
restore: backup-tools-build
$(BACKUP_DOCKER_RUN) bash -lc 'BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" ALLOW_NON_EMPTY_RESTORE="$(ALLOW_NON_EMPTY_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore.sh'
restore: backup-tools-build ensure-restore-containers ## 백업 덤프 복구
$(BACKUP_DOCKER_RUN) bash -lc 'RESTORE_INPUT="$(RESTORE_INPUT)" BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" ALLOW_NON_EMPTY_RESTORE="$(ALLOW_NON_EMPTY_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore.sh'
dump-verify: backup-tools-build
dump-verify: backup-tools-build ## 백업 덤프 검증
$(BACKUP_DOCKER_RUN) bash -lc 'BACKUP="$(BACKUP)" scripts/backup/verify-dump.sh'
restore-verify: backup-tools-build
restore-verify: backup-tools-build ## 복구 결과 검증
$(BACKUP_DOCKER_RUN) bash -lc 'BACKUP="$(BACKUP)" scripts/backup/verify-restore.sh'
dump-list: backup-tools-build
dump-list: backup-tools-build ## 사용 가능한 백업 덤프 목록 조회
$(BACKUP_DOCKER_RUN) bash -lc 'BACKUP_ROOT="$(BACKUP_ROOT)" scripts/backup/dump-list.sh'
restore-plan: backup-tools-build
$(BACKUP_DOCKER_RUN) bash -lc 'BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore-plan.sh'
restore-plan: backup-tools-build ## 복구 실행 계획 출력
$(BACKUP_DOCKER_RUN) bash -lc 'RESTORE_INPUT="$(RESTORE_INPUT)" BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore-plan.sh'
upload-cloud: backup-tools-build
upload-cloud: backup-tools-build ## 백업 덤프 클라우드 업로드
$(BACKUP_DOCKER_RUN) bash -lc 'WORKS_DRIVE_DRY_RUN="$(WORKS_DRIVE_DRY_RUN)" BACKUP="$(BACKUP)" scripts/backup/upload_cloud.sh'
else
dump:
dump: ## 백업 덤프 생성
DUMP_SERVICES="$(DUMP_SERVICES)" DUMP_MODE="$(DUMP_MODE)" BACKUP="$(BACKUP)" BACKUP_ROOT="$(BACKUP_ROOT)" scripts/backup/dump.sh
restore:
BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" ALLOW_NON_EMPTY_RESTORE="$(ALLOW_NON_EMPTY_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore.sh
restore: ensure-restore-containers ## 백업 덤프 복구
RESTORE_INPUT="$(RESTORE_INPUT)" BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" ALLOW_NON_EMPTY_RESTORE="$(ALLOW_NON_EMPTY_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore.sh
dump-verify:
dump-verify: ## 백업 덤프 검증
BACKUP="$(BACKUP)" scripts/backup/verify-dump.sh
restore-verify:
restore-verify: ## 복구 결과 검증
BACKUP="$(BACKUP)" scripts/backup/verify-restore.sh
dump-list:
dump-list: ## 사용 가능한 백업 덤프 목록 조회
BACKUP_ROOT="$(BACKUP_ROOT)" scripts/backup/dump-list.sh
restore-plan:
BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore-plan.sh
restore-plan: ## 복구 실행 계획 출력
RESTORE_INPUT="$(RESTORE_INPUT)" BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore-plan.sh
upload-cloud:
upload-cloud: ## 백업 덤프 클라우드 업로드
WORKS_DRIVE_DRY_RUN="$(WORKS_DRIVE_DRY_RUN)" BACKUP="$(BACKUP)" scripts/backup/upload_cloud.sh
endif
dump-upload-cloud: dump upload-cloud
dump-upload-cloud: dump upload-cloud ## 백업 덤프 생성 후 클라우드 업로드
# --- 로컬 통합 코드 체크 ---
PLAYWRIGHT_BROWSERS_PATH := $(HOME)/.cache/ms-playwright
@@ -268,12 +370,12 @@ CODE_CHECK_TEST_JOBS ?= 1
PLAYWRIGHT_WORKERS ?= 1
FLUTTER_TEST_CONCURRENCY ?= 1
code-check: code-check-lint code-check-test-jobs
code-check: code-check-lint code-check-test-jobs ## 로컬 CI 상당 코드 검사 실행
@echo "code-check complete."
code-check-lint: code-check-i18n code-check-i18n-values code-check-front-lint code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint
code-check-lint: code-check-i18n code-check-i18n-values code-check-front-lint code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint ## 로컬 린트와 정적 검사 실행
code-check-test-jobs:
code-check-test-jobs: ## 코드 검사 테스트 작업 실행
@echo "==> run CI-equivalent test jobs (parallel)"
@$(MAKE) --no-print-directory -j$(CODE_CHECK_TEST_JOBS) --output-sync=target \
code-check-backend-tests \
@@ -283,20 +385,20 @@ code-check-test-jobs:
code-check-devfront-tests \
code-check-orgfront-tests
code-check-i18n:
code-check-i18n: ## i18n 리소스 검사
@echo "==> i18n resource check"
@mkdir -p reports
node tools/i18n-scanner/index.js
node tools/i18n-scanner/report.js
@cat reports/i18n-report.txt
code-check-i18n-values:
code-check-i18n-values: ## i18n 번역 값 품질 검사
@echo "==> i18n value quality check"
@mkdir -p reports
node tools/i18n-scanner/value-check.js
@cat reports/i18n-value-report.txt
code-check-go-lint:
code-check-go-lint: ## Go 포맷과 린트 검사
@echo "==> go lint/format check"
@if command -v golangci-lint >/dev/null 2>&1; then \
cd backend && golangci-lint fmt -E gofmt -E gofumpt -d; \
@@ -312,11 +414,11 @@ code-check-go-lint:
exit 1; \
fi
code-check-sync-userfront-locales:
code-check-sync-userfront-locales: ## UserFront 로케일 동기화 검사
@echo "==> sync userfront locales"
/bin/sh ./scripts/sync_userfront_locales.sh
code-check-userfront-install:
code-check-userfront-install: ## UserFront 의존성 설치
@echo "==> install userfront dependencies"
@if command -v flutter >/dev/null 2>&1; then \
cd userfront && flutter pub get; \
@@ -324,7 +426,7 @@ code-check-userfront-install:
echo "WARNING: flutter not found, skipping userfront dependencies install."; \
fi
code-check-userfront-lint:
code-check-userfront-lint: ## UserFront 포맷과 analyze 검사
@echo "==> userfront format/analyze"
@if command -v dart >/dev/null 2>&1; then \
cd userfront && dart format --output=none --set-exit-if-changed lib test; \
@@ -337,7 +439,7 @@ code-check-userfront-lint:
echo "WARNING: flutter not found, skipping userfront analyze."; \
fi
code-check-front-lint:
code-check-front-lint: ## 프론트엔드 Biome 린트와 포맷 검사
@echo "==> adminfront biome lint/format check"
rm -rf adminfront/playwright-report adminfront/test-results
@if [ -d adminfront/node_modules ]; then \
@@ -366,11 +468,11 @@ code-check-front-lint:
cd orgfront && ./node_modules/@biomejs/biome/bin/biome lint .
cd orgfront && ./node_modules/@biomejs/biome/bin/biome format .
code-check-backend-tests:
code-check-backend-tests: ## 백엔드 Go 테스트 실행
@echo "==> backend tests"
cd backend && GOCACHE=/tmp/baron-sso-go-cache go test -v ./...
code-check-userfront-tests:
code-check-userfront-tests: ## UserFront Flutter 테스트 실행
@echo "==> userfront tests (isolated workspace)"
@if ! command -v flutter >/dev/null 2>&1; then \
echo "WARNING: flutter not found, skipping userfront tests."; \
@@ -396,11 +498,11 @@ code-check-userfront-tests:
cd "$$tmp_dir" && /bin/sh ./scripts/sync_userfront_locales.sh; \
cd "$$tmp_dir/userfront" && flutter test --concurrency=$(FLUTTER_TEST_CONCURRENCY)
code-check-adminfront-tests:
code-check-adminfront-tests: ## AdminFront 테스트 실행
@echo "==> adminfront tests"
PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) ./scripts/run_adminfront_ci_tests.sh adminfront-tests
code-check-devfront-tests:
code-check-devfront-tests: ## DevFront 테스트 실행
@echo "==> devfront tests"
@mkdir -p reports/devfront
@rm -rf reports/devfront/playwright-report reports/devfront/test-results
@@ -423,7 +525,7 @@ code-check-devfront-tests:
[ -d devfront/test-results ] && cp -R devfront/test-results reports/devfront/ || true; \
exit $$status
code-check-orgfront-tests:
code-check-orgfront-tests: ## OrgFront 테스트 실행
@echo "==> orgfront tests"
@mkdir -p reports/orgfront
@rm -rf reports/orgfront/playwright-report reports/orgfront/test-results
@@ -439,7 +541,7 @@ code-check-orgfront-tests:
[ -d orgfront/test-results ] && cp -R orgfront/test-results reports/orgfront/ || true; \
exit $$status
code-check-userfront-e2e-tests:
code-check-userfront-e2e-tests: ## UserFront WASM E2E 테스트 실행
@echo "==> userfront wasm playwright e2e tests (isolated workspace)"
@if ! command -v flutter >/dev/null 2>&1; then \
echo "WARNING: flutter not found, skipping userfront e2e tests."; \

View File

@@ -64,14 +64,6 @@ vi.mock("../../lib/adminApi", () => ({
total: 1,
})),
fetchOrySSOTSystemStatus: vi.fn(async () => ({
userProjection: {
name: "kratos_users",
status: "ready",
ready: true,
lastSyncedAt: "2026-05-11T03:00:00Z",
updatedAt: "2026-05-11T03:00:10Z",
projectedUsers: 152,
},
identityCache: {
status: "ready",
redisReady: true,
@@ -158,8 +150,8 @@ describe("DataIntegrityPage", () => {
).toBeGreaterThan(0);
expect(await screen.findByText("Redis identity cache")).toBeInTheDocument();
expect(screen.getAllByText("준비됨").length).toBeGreaterThan(0);
expect(screen.getByText("152")).toBeInTheDocument();
expect(screen.getByText("151")).toBeInTheDocument();
expect(screen.queryByText("Local users")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /Redis cache flush/ }));
await waitFor(() => {

View File

@@ -15,14 +15,6 @@ let currentRole = "super_admin";
vi.mock("../../lib/adminApi", () => ({
fetchMe: vi.fn(async () => ({ role: currentRole })),
fetchOrySSOTSystemStatus: vi.fn(async () => ({
userProjection: {
name: "kratos_users",
status: "ready",
ready: true,
lastSyncedAt: "2026-05-11T03:00:00Z",
updatedAt: "2026-05-11T03:00:10Z",
projectedUsers: 152,
},
identityCache: {
status: "ready",
redisReady: true,
@@ -74,8 +66,9 @@ describe("UserProjectionPage", () => {
expect(await screen.findByText("Redis identity cache")).toBeInTheDocument();
expect(screen.getAllByText("준비됨").length).toBeGreaterThan(0);
expect(screen.getByText("관측 identity")).toBeInTheDocument();
expect(screen.getByText("152")).toBeInTheDocument();
expect(screen.getByText("151")).toBeInTheDocument();
expect(screen.queryByText("Local users")).not.toBeInTheDocument();
expect(screen.queryByText("Backend 사용자 read model")).not.toBeInTheDocument();
expect(fetchOrySSOTSystemStatus).toHaveBeenCalled();
});

View File

@@ -72,7 +72,6 @@ export function UserProjectionContent({
if (confirmed) flushMutation.mutate();
};
const projection = data?.userProjection;
const identityCache = data?.identityCache;
const header = (
@@ -146,79 +145,6 @@ export function UserProjectionContent({
</section>
) : null}
<section className="rounded-lg border border-border bg-card p-5">
<div className="flex items-center gap-3 border-b border-border pb-4">
<div>
<h3 className="text-lg font-bold">
{t(
"ui.admin.ory_ssot.projection_card.title",
"Backend user read model",
)}
</h3>
<p className="text-sm text-muted-foreground">
{t(
"ui.admin.ory_ssot.projection_card.description",
"PostgreSQL read model status used by admin search and statistics.",
)}
</p>
</div>
</div>
{isLoading ? (
<div className="py-8 text-sm text-muted-foreground">
{t("ui.admin.ory_ssot.loading", "Loading")}
</div>
) : (
<dl className="grid gap-4 py-5 sm:grid-cols-2 lg:grid-cols-4">
<div>
<dt className="text-sm text-muted-foreground">
{t("ui.admin.ory_ssot.summary.status", "Status")}
</dt>
<dd className="mt-1">
<StatusBadge
ready={projection?.ready ?? false}
status={projection?.status ?? "unknown"}
/>
</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">
{t("ui.admin.ory_ssot.summary.local_users", "Local users")}
</dt>
<dd className="mt-1 text-xl font-semibold tabular-nums">
{projection?.projectedUsers ?? 0}
</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">
{t(
"ui.admin.ory_ssot.summary.last_synced",
"Last read-model refresh",
)}
</dt>
<dd className="mt-1 text-sm">
{formatDateTime(projection?.lastSyncedAt)}
</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">
{t("ui.admin.ory_ssot.summary.updated_at", "Updated at")}
</dt>
<dd className="mt-1 text-sm">
{formatDateTime(projection?.updatedAt)}
</dd>
</div>
</dl>
)}
{projection?.lastError ? (
<div className="flex gap-2 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-200">
<AlertTriangle className="mt-0.5 shrink-0" size={16} />
<span>{projection.lastError}</span>
</div>
) : null}
</section>
<section className="rounded-lg border border-border bg-card p-5">
<div className="flex items-center gap-3 border-b border-border pb-4">
<div>

View File

@@ -10,6 +10,7 @@ import {
filterWorksmobileComparisonRowsBySearch,
formatWorksmobileOrgDetails,
formatWorksmobilePersonName,
formatWorksmobileSelectionFailureDescription,
formatWorksmobileUpdateDetails,
getDefaultGroupComparisonFilters,
getDefaultUserComparisonFilters,
@@ -509,6 +510,48 @@ describe("TenantWorksmobilePage comparison helpers", () => {
).toEqual([rows[0]]);
});
it("filters users by WORKS account status", () => {
const rows = [
{
resourceType: "USER",
status: "matched",
baronId: "user-1",
worksmobileAccountStatus: "active",
},
{
resourceType: "USER",
status: "matched",
baronId: "user-2",
worksmobileAccountStatus: "suspended",
},
{
resourceType: "USER",
status: "matched",
baronId: "user-3",
worksmobileAccountStatus: "invited",
},
];
expect(
filterWorksmobileComparisonRows(
rows,
getDefaultUserComparisonFilters(),
false,
"suspended",
),
).toEqual([rows[1]]);
});
it("formats partial Worksmobile selection failures with detailed reasons", () => {
expect(
formatWorksmobileSelectionFailureDescription(1, [
"7e30daf6-f912-4306-befc-478feb7b74cc: target user tenant is excluded from Worksmobile sync",
]),
).toBe(
"성공 1건, 실패 1건\n7e30daf6-f912-4306-befc-478feb7b74cc: target user tenant is excluded from Worksmobile sync",
);
});
it("formats update details for changed organization rows", () => {
expect(
formatWorksmobileUpdateDetails({
@@ -529,6 +572,28 @@ describe("TenantWorksmobilePage comparison helpers", () => {
]);
});
it("formats update details for changed user phone and employee number", () => {
expect(
formatWorksmobileUpdateDetails({
resourceType: "USER",
status: "needs_update",
baronId: "user-1",
baronName: "강명진",
worksmobileName: "강명진",
baronEmail: "mjkang4@hanmaceng.co.kr",
worksmobileEmail: "mjkang4@hanmaceng.co.kr",
externalKey: "user-1",
baronPhone: "+821051583696",
worksmobilePhone: "+821099998888",
baronEmployeeNumber: "mjkang4",
worksmobileEmployeeNumber: "M17205",
}),
).toEqual([
"전화번호: +821099998888 -> +821051583696",
"사번: M17205 -> mjkang4",
]);
});
it("formats WORKS account name with level on one line", () => {
expect(
formatWorksmobilePersonName({

View File

@@ -66,6 +66,7 @@ import {
filterWorksmobileComparisonRowsBySearch,
formatWorksmobileOrgDetails,
formatWorksmobilePersonName,
formatWorksmobileSelectionFailureDescription,
formatWorksmobileUpdateDetails,
getDefaultGroupComparisonFilters,
getDefaultUserComparisonFilters,
@@ -77,10 +78,12 @@ import {
getWorksmobileSelectedUpdateUserIds,
getWorksmobileSelectedWorksOnlyOrgUnitIds,
summarizeWorksmobileComparison,
type WorksmobileAccountStatusFilter,
type WorksmobileComparisonColumnKey,
type WorksmobileComparisonColumnVisibility,
type WorksmobileComparisonFilter,
type WorksmobileComparisonSummary,
worksmobileAccountStatusFilterOptions,
} from "./worksmobileComparison";
function worksmobileJobPayloadString(job: WorksmobileOutboxItem, key: string) {
@@ -183,6 +186,8 @@ export function TenantWorksmobilePage() {
const [groupFilters, setGroupFilters] = React.useState<
WorksmobileComparisonFilter[]
>(getDefaultGroupComparisonFilters);
const [userAccountStatusFilter, setUserAccountStatusFilter] =
React.useState<WorksmobileAccountStatusFilter>("all");
const [includeUserMissingExternalKey, setIncludeUserMissingExternalKey] =
React.useState(false);
const [includeGroupMissingExternalKey, setIncludeGroupMissingExternalKey] =
@@ -323,10 +328,11 @@ export function TenantWorksmobilePage() {
return {
resourceKind,
count: successCount,
failures,
failureCount: failures.length,
};
},
onSuccess: ({ resourceKind, count, failureCount }) => {
onSuccess: ({ resourceKind, count, failureCount, failures }) => {
if (resourceKind === "users") {
setSelectedUserRowKeys([]);
} else {
@@ -334,7 +340,10 @@ export function TenantWorksmobilePage() {
}
if (failureCount > 0) {
toast.error("일부 WORKS 생성 작업 등록 실패", {
description: `성공 ${count}건, 실패 ${failureCount}`,
description: formatWorksmobileSelectionFailureDescription(
count,
failures,
),
});
} else {
toast.success("WORKS 생성 작업을 등록했습니다.", {
@@ -418,6 +427,7 @@ export function TenantWorksmobilePage() {
comparisonUsers,
userFilters,
includeUserMissingExternalKey,
userAccountStatusFilter,
),
userSearch,
);
@@ -643,6 +653,11 @@ export function TenantWorksmobilePage() {
setUserFilters(nextFilters);
setSelectedUserRowKeys([]);
}}
accountStatusFilter={userAccountStatusFilter}
onAccountStatusFilterChange={(nextStatus) => {
setUserAccountStatusFilter(nextStatus);
setSelectedUserRowKeys([]);
}}
baronOrgColumnLabel="대표 Baron 조직"
includeMissingExternalKey={includeUserMissingExternalKey}
onIncludeMissingExternalKeyChange={(checked) => {
@@ -988,6 +1003,8 @@ function ComparisonTable({
searchPlaceholder = "이름 또는 UUID 검색",
filters,
onFiltersChange,
accountStatusFilter,
onAccountStatusFilterChange,
baronOrgColumnLabel = "Baron 조직",
includeMissingExternalKey,
onIncludeMissingExternalKeyChange,
@@ -1018,6 +1035,10 @@ function ComparisonTable({
searchPlaceholder?: string;
filters?: WorksmobileComparisonFilter[];
onFiltersChange?: (filters: WorksmobileComparisonFilter[]) => void;
accountStatusFilter?: WorksmobileAccountStatusFilter;
onAccountStatusFilterChange?: (
status: WorksmobileAccountStatusFilter,
) => void;
baronOrgColumnLabel?: string;
includeMissingExternalKey?: boolean;
onIncludeMissingExternalKeyChange?: (checked: boolean) => void;
@@ -1277,6 +1298,29 @@ function ComparisonTable({
) : null
}
/>
{accountStatusFilter && onAccountStatusFilterChange ? (
<div
className="flex flex-wrap items-center gap-2"
role="tablist"
aria-label="WORKS 계정 상태"
>
{worksmobileAccountStatusFilterOptions.map((option) => (
<Button
key={option.value}
type="button"
role="tab"
size="sm"
variant={
accountStatusFilter === option.value ? "default" : "outline"
}
aria-selected={accountStatusFilter === option.value}
onClick={() => onAccountStatusFilterChange(option.value)}
>
{option.label}
</Button>
))}
</div>
) : null}
</div>
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
<Dialog
@@ -1603,6 +1647,13 @@ function ComparisonTable({
>
{getWorksmobileComparisonStatusLabel(row.status)}
</Badge>
{row.worksmobileAccountStatus && (
<div className="mt-1">
<Badge variant="outline">
WORKS {row.worksmobileAccountStatus}
</Badge>
</div>
)}
{formatWorksmobileUpdateDetails(row).map((detail) => (
<div
key={detail}

View File

@@ -6,6 +6,14 @@ export type WorksmobileComparisonFilter =
| "needs_update"
| "matched";
export type WorksmobileAccountStatusFilter =
| "all"
| "active"
| "invited"
| "suspended"
| "inactive"
| "deleted";
export type WorksmobileComparisonSummary = {
total: number;
matched: number;
@@ -204,6 +212,22 @@ export function getWorksmobileSelectedUpdateUserIds(
.filter((id): id is string => Boolean(id));
}
export function formatWorksmobileSelectionFailureDescription(
successCount: number,
failures: string[],
) {
const summary = `성공 ${successCount}건, 실패 ${failures.length}`;
const visibleFailures = failures.slice(0, 3);
if (failures.length <= visibleFailures.length) {
return [summary, ...visibleFailures].join("\n");
}
return [
summary,
...visibleFailures,
`${failures.length - visibleFailures.length}건 실패`,
].join("\n");
}
export function getWorksmobileSelectedMissingExternalKeyOrgUnitIds(
rows: WorksmobileComparisonItem[],
selectedKeys: string[],
@@ -251,6 +275,7 @@ const worksmobileComparisonSearchFields: Array<
"externalKey",
"worksmobileName",
"worksmobileEmail",
"worksmobileAccountStatus",
"worksmobileLevelId",
"worksmobileLevelName",
"worksmobileTask",
@@ -292,6 +317,7 @@ export function filterWorksmobileComparisonRows(
rows: WorksmobileComparisonItem[],
filters: WorksmobileComparisonFilter[],
onlyMissingExternalKey = false,
accountStatus: WorksmobileAccountStatusFilter = "all",
) {
const allowedStatuses = new Set(
filters.flatMap((filter) => worksmobileFilterStatuses[filter]),
@@ -302,7 +328,15 @@ export function filterWorksmobileComparisonRows(
}
allowedStatuses.add("missing_external_key");
}
return rows.filter((row) => allowedStatuses.has(row.status));
return rows.filter((row) => {
if (accountStatus !== "all") {
return row.worksmobileAccountStatus === accountStatus;
}
if (!allowedStatuses.has(row.status)) {
return false;
}
return true;
});
}
export function formatWorksmobilePersonName(row: WorksmobileComparisonItem) {
@@ -358,6 +392,22 @@ export function formatWorksmobileUpdateDetails(row: WorksmobileComparisonItem) {
if (expectedEmail && actualEmail && expectedEmail !== actualEmail) {
details.push(`이메일: ${actualEmail} -> ${expectedEmail}`);
}
const expectedPhone = row.baronPhone?.trim() ?? "";
const actualPhone = row.worksmobilePhone?.trim() ?? "";
if (expectedPhone && actualPhone && expectedPhone !== actualPhone) {
details.push(`전화번호: ${actualPhone} -> ${expectedPhone}`);
}
const expectedEmployeeNumber = row.baronEmployeeNumber?.trim() ?? "";
const actualEmployeeNumber = row.worksmobileEmployeeNumber?.trim() ?? "";
if (
expectedEmployeeNumber &&
actualEmployeeNumber &&
expectedEmployeeNumber !== actualEmployeeNumber
) {
details.push(
`사번: ${actualEmployeeNumber} -> ${expectedEmployeeNumber}`,
);
}
return details;
}
@@ -445,6 +495,18 @@ export const comparisonFilterOptions: Array<{
export const userFilterOptions = comparisonFilterOptions;
export const worksmobileAccountStatusFilterOptions: Array<{
value: WorksmobileAccountStatusFilter;
label: string;
}> = [
{ value: "all", label: "WORKS 전체" },
{ value: "active", label: "active" },
{ value: "invited", label: "invited" },
{ value: "suspended", label: "suspended" },
{ value: "inactive", label: "inactive" },
{ value: "deleted", label: "deleted" },
];
export function getDefaultUserComparisonFilters(): WorksmobileComparisonFilter[] {
return ["baron_only", "needs_update", "works_only"];
}

View File

@@ -159,6 +159,7 @@ export type UserProjectionStatus = {
export type IdentityCacheStatus = {
status: string;
redisReady: boolean;
mirrorVersion?: string;
observedCount: number;
keyCount: number;
lastRefreshedAt?: string;
@@ -167,7 +168,6 @@ export type IdentityCacheStatus = {
};
export type OrySSOTSystemStatus = {
userProjection: UserProjectionStatus;
identityCache: IdentityCacheStatus;
};
@@ -884,6 +884,8 @@ export type WorksmobileComparisonItem = {
baronSlug?: string;
baronName?: string;
baronEmail?: string;
baronPhone?: string;
baronEmployeeNumber?: string;
baronPrimaryOrgId?: string;
baronPrimaryOrgSlug?: string;
baronPrimaryOrgName?: string;
@@ -894,6 +896,9 @@ export type WorksmobileComparisonItem = {
externalKey?: string;
worksmobileName?: string;
worksmobileEmail?: string;
worksmobilePhone?: string;
worksmobileEmployeeNumber?: string;
worksmobileAccountStatus?: string;
worksmobileLevelId?: string;
worksmobileLevelName?: string;
worksmobileTask?: string;

View File

@@ -1,8 +1,11 @@
package main
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/service"
"context"
"encoding/csv"
"errors"
"strings"
"testing"
)
@@ -120,6 +123,339 @@ func TestAuditWorksmobileDuplicatePhoneCountryCodesReportsAndFixes(t *testing.T)
}
}
func TestRecreatePendingWorksmobileUsersFromSnapshotCreatesOnlyMatchedUsers(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "11111111-1111-1111-1111-111111111111"
tenantID := "22222222-2222-2222-2222-222222222222"
userID := "33333333-3333-3333-3333-333333333333"
client := &fakeWorksmobilePendingRecreateClient{}
output := &strings.Builder{}
writer := csv.NewWriter(output)
counts, err := recreatePendingWorksmobileUsersFromSnapshot(
context.Background(),
[]service.WorksmobileRemoteUser{
{Email: "matched@samaneng.com", ID: "works-1", ExternalID: userID, DisplayName: "Matched"},
{Email: "missing@samaneng.com", ID: "works-2", ExternalID: "44444444-4444-4444-4444-444444444444", DisplayName: "Missing"},
},
func(ctx context.Context, remote service.WorksmobileRemoteUser) (domain.User, bool) {
if remote.ExternalID != userID {
return domain.User{}, false
}
return domain.User{
ID: userID,
Email: "matched@samaneng.com",
Name: "Matched User",
Status: domain.UserStatusActive,
TenantID: &tenantID,
}, true
},
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany},
tenantID: {ID: tenantID, Slug: "team", Name: "Team", Type: domain.TenantTypeOrganization, ParentID: &rootID},
},
nil,
"hanmac-family2026",
0,
writer,
client,
)
if err != nil {
t.Fatalf("recreatePendingWorksmobileUsersFromSnapshot returned error: %v", err)
}
if counts.OK != 1 || counts.Skipped != 1 || counts.Errors != 0 {
t.Fatalf("counts=%+v, want ok=1 skipped=1 errors=0", counts)
}
if len(client.patchedUsers) != 1 || client.patchedUsers[0].identifier != "matched@samaneng.com" {
t.Fatalf("patched users=%v", client.patchedUsers)
}
if !strings.Contains(client.patchedUsers[0].payload.Email, ".old") {
t.Fatalf("tombstone email=%q", client.patchedUsers[0].payload.Email)
}
if len(client.patchedUsers[0].payload.AliasEmails) != 0 {
t.Fatalf("tombstone alias emails were not cleared: %v", client.patchedUsers[0].payload.AliasEmails)
}
if len(client.patchedUsers[0].payload.Organizations) == 0 || client.patchedUsers[0].payload.Organizations[0].Email != client.patchedUsers[0].payload.Email {
t.Fatalf("tombstone organization email was not updated: %+v", client.patchedUsers[0].payload.Organizations)
}
if len(client.deletedUsers) != 1 || client.deletedUsers[0] != client.patchedUsers[0].payload.Email {
t.Fatalf("deleted users=%v", client.deletedUsers)
}
if len(client.createdUsers) != 1 {
t.Fatalf("created users=%d, want 1", len(client.createdUsers))
}
if client.createdUsers[0].PasswordConfig.Password != "hanmac-family2026" {
t.Fatal("initial password was not applied to recreated user")
}
if strings.Contains(output.String(), "missing@samaneng.com") && !strings.Contains(output.String(), "baron user not found") {
t.Fatalf("missing user skip reason was not written: %s", output.String())
}
}
func TestRecreatePendingWorksmobileUsersFromSnapshotRollsBackWhenCreateFails(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "11111111-1111-1111-1111-111111111111"
tenantID := "22222222-2222-2222-2222-222222222222"
userID := "33333333-3333-3333-3333-333333333333"
client := &fakeWorksmobilePendingRecreateClient{createErr: errors.New("create failed")}
output := &strings.Builder{}
writer := csv.NewWriter(output)
counts, err := recreatePendingWorksmobileUsersFromSnapshot(
context.Background(),
[]service.WorksmobileRemoteUser{{Email: "matched@samaneng.com", ID: "works-1", ExternalID: userID}},
func(ctx context.Context, remote service.WorksmobileRemoteUser) (domain.User, bool) {
return domain.User{
ID: userID,
Email: "matched@samaneng.com",
Name: "Matched User",
Status: domain.UserStatusActive,
TenantID: &tenantID,
}, true
},
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany},
tenantID: {ID: tenantID, Slug: "team", Name: "Team", Type: domain.TenantTypeOrganization, ParentID: &rootID},
},
nil,
"hanmac-family2026",
0,
writer,
client,
)
if err != nil {
t.Fatalf("recreatePendingWorksmobileUsersFromSnapshot returned error: %v", err)
}
if counts.OK != 0 || counts.Errors != 1 {
t.Fatalf("counts=%+v, want ok=0 errors=1", counts)
}
if len(client.patchedUsers) != 2 {
t.Fatalf("patched users=%v", client.patchedUsers)
}
if client.patchedUsers[1].payload.Email != "matched@samaneng.com" {
t.Fatalf("rollback email=%q, want matched@samaneng.com", client.patchedUsers[1].payload.Email)
}
if !strings.Contains(output.String(), "create failed") || !strings.Contains(output.String(), "ok") {
t.Fatalf("rollback result was not written: %s", output.String())
}
}
func TestImportHanmacWorksmobileUsersFromRowsSkipsExistingRemoteLocalPart(t *testing.T) {
t.Setenv("HANMAC_DOMAIN_ID", "300286336")
rootID := "11111111-1111-1111-1111-111111111111"
companyID := "22222222-2222-2222-2222-222222222222"
tenantID := "33333333-3333-3333-3333-333333333333"
client := &fakeWorksmobilePendingRecreateClient{}
store := &fakeHanmacWorksmobileUserStore{}
output := &strings.Builder{}
writer := csv.NewWriter(output)
counts, err := importHanmacWorksmobileUsersFromRows(
context.Background(),
[]hanmacWorksmobileImportRow{{
Email: "new@hanmaceng.co.kr",
Name: "New User",
Role: "user",
TenantSlug: "infra-structures",
EmployeeID: "M25001",
SubEmail: "legacy@hanmaceng.co.kr",
}},
domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
map[string]domain.Tenant{
"infra-structures": {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
companyID: {ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID},
tenantID: {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
[]service.WorksmobileRemoteUser{{
Email: "owner@hanmaceng.co.kr",
AliasEmails: []string{"legacy@hanmaceng.co.kr"},
}},
nil,
store,
"hanmac-family2026",
0,
true,
writer,
client,
)
if err != nil {
t.Fatalf("importHanmacWorksmobileUsersFromRows returned error: %v", err)
}
if counts.OK != 0 || counts.Skipped != 1 || counts.Errors != 0 {
t.Fatalf("counts=%+v, want ok=0 skipped=1 errors=0", counts)
}
if len(store.saved) != 0 {
t.Fatalf("saved users=%d, want 0", len(store.saved))
}
if len(client.createdUsers) != 0 {
t.Fatalf("created Worksmobile users=%d, want 0", len(client.createdUsers))
}
if !strings.Contains(output.String(), "legacy") || !strings.Contains(output.String(), "local-part already exists") {
t.Fatalf("result did not include conflict reason: %s", output.String())
}
}
func TestImportHanmacWorksmobileUsersFromRowsSavesBaronUserAndCreatesWorksmobileUser(t *testing.T) {
t.Setenv("HANMAC_DOMAIN_ID", "300286336")
rootID := "11111111-1111-1111-1111-111111111111"
companyID := "22222222-2222-2222-2222-222222222222"
tenantID := "33333333-3333-3333-3333-333333333333"
client := &fakeWorksmobilePendingRecreateClient{}
store := &fakeHanmacWorksmobileUserStore{}
output := &strings.Builder{}
writer := csv.NewWriter(output)
counts, err := importHanmacWorksmobileUsersFromRows(
context.Background(),
[]hanmacWorksmobileImportRow{{
Email: "new@hanmaceng.co.kr",
Name: "New User",
Phone: "010-1234-5678",
Role: "user",
TenantSlug: "infra-structures",
Grade: "과장",
EmployeeID: "M25001",
SubEmail: "new.alias@hanmaceng.co.kr",
}},
domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
map[string]domain.Tenant{
"infra-structures": {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
companyID: {ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID},
tenantID: {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
nil,
nil,
store,
"hanmac-family2026",
0,
true,
writer,
client,
)
if err != nil {
t.Fatalf("importHanmacWorksmobileUsersFromRows returned error: %v", err)
}
if counts.OK != 1 || counts.Skipped != 0 || counts.Errors != 0 || counts.BaronCreated != 1 {
t.Fatalf("counts=%+v, want ok=1 baronCreated=1", counts)
}
if len(store.saved) != 1 {
t.Fatalf("saved users=%d, want 1", len(store.saved))
}
if store.saved[0].TenantID == nil || *store.saved[0].TenantID != tenantID {
t.Fatalf("saved tenant=%v, want %s", store.saved[0].TenantID, tenantID)
}
if store.saved[0].Metadata["employee_id"] != "M25001" || store.saved[0].Metadata["sub_email"] != "new.alias@hanmaceng.co.kr" {
t.Fatalf("metadata=%v", store.saved[0].Metadata)
}
if len(client.createdUsers) != 1 {
t.Fatalf("created Worksmobile users=%d, want 1", len(client.createdUsers))
}
if client.createdUsers[0].Email != "new@hanmaceng.co.kr" {
t.Fatalf("created email=%q", client.createdUsers[0].Email)
}
if client.createdUsers[0].PasswordConfig.Password != "hanmac-family2026" {
t.Fatal("initial password was not applied")
}
if !strings.Contains(strings.Join(client.createdUsers[0].AliasEmails, ","), "new.alias@hanmaceng.co.kr") {
t.Fatalf("alias emails=%v", client.createdUsers[0].AliasEmails)
}
}
func TestImportHanmacWorksmobileUsersFromRowsKeepsExternalSubEmailOutOfWorksmobileAliases(t *testing.T) {
t.Setenv("HANMAC_DOMAIN_ID", "300286336")
rootID := "11111111-1111-1111-1111-111111111111"
companyID := "22222222-2222-2222-2222-222222222222"
tenantID := "33333333-3333-3333-3333-333333333333"
client := &fakeWorksmobilePendingRecreateClient{}
store := &fakeHanmacWorksmobileUserStore{}
output := &strings.Builder{}
writer := csv.NewWriter(output)
counts, err := importHanmacWorksmobileUsersFromRows(
context.Background(),
[]hanmacWorksmobileImportRow{{
Email: "external-alias@hanmaceng.co.kr",
Name: "External Alias",
Role: "user",
TenantSlug: "infra-structures",
EmployeeID: "M25002",
SubEmail: "external@gmail.com",
}},
domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
map[string]domain.Tenant{
"infra-structures": {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
companyID: {ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID},
tenantID: {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
nil,
nil,
store,
"hanmac-family2026",
0,
true,
writer,
client,
)
if err != nil {
t.Fatalf("importHanmacWorksmobileUsersFromRows returned error: %v", err)
}
if counts.OK != 1 || counts.Errors != 0 || counts.Skipped != 0 {
t.Fatalf("counts=%+v, want ok=1", counts)
}
if store.saved[0].Metadata["sub_email"] != nil {
t.Fatalf("external sub_email should not be stored as Worksmobile alias metadata: %v", store.saved[0].Metadata)
}
if store.saved[0].Metadata["external_sub_email"] != "external@gmail.com" {
t.Fatalf("external_sub_email=%v", store.saved[0].Metadata["external_sub_email"])
}
if strings.Contains(strings.Join(client.createdUsers[0].AliasEmails, ","), "external@gmail.com") {
t.Fatalf("external sub email was sent as alias: %v", client.createdUsers[0].AliasEmails)
}
}
func TestBuildAdminctlWorksmobileOrgUnitPayloadClearsDomainRootParent(t *testing.T) {
t.Setenv("HANMAC_DOMAIN_ID", "300286336")
rootID := "11111111-1111-1111-1111-111111111111"
companyID := "22222222-2222-2222-2222-222222222222"
orgID := "33333333-3333-3333-3333-333333333333"
root := domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"}
company := domain.Tenant{ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID}
org := domain.Tenant{ID: orgID, Slug: "management-support", Name: "경영지원부", Type: domain.TenantTypeOrganization, ParentID: &companyID}
payload, err := buildAdminctlWorksmobileOrgUnitPayload(org, root, map[string]domain.Tenant{
rootID: root,
companyID: company,
orgID: org,
})
if err != nil {
t.Fatalf("buildAdminctlWorksmobileOrgUnitPayload returned error: %v", err)
}
if payload.DomainID != 300286336 {
t.Fatalf("domainID=%d, want 300286336", payload.DomainID)
}
if payload.Email != "management-support@hanmaceng.co.kr" {
t.Fatalf("email=%q, want management-support@hanmaceng.co.kr", payload.Email)
}
if payload.ParentOrgUnitID != "" {
t.Fatalf("parentOrgUnitID=%q, want empty for domain-root child", payload.ParentOrgUnitID)
}
}
type fakeWorksmobilePhoneAuditClient struct {
users []service.WorksmobileRemoteUser
patches []fakeWorksmobilePhonePatch
@@ -138,3 +474,100 @@ func (f *fakeWorksmobilePhoneAuditClient) PatchUser(ctx context.Context, identif
f.patches = append(f.patches, fakeWorksmobilePhonePatch{identifier: identifier, payload: payload})
return nil
}
type fakeWorksmobilePendingRecreateClient struct {
createdUsers []service.WorksmobileUserPayload
deletedUsers []string
undeletedUsers []string
patchedUsers []fakeWorksmobilePendingRecreatePatch
createErr error
}
type fakeWorksmobilePendingRecreatePatch struct {
identifier string
payload service.WorksmobileUserPatchPayload
}
func (f *fakeWorksmobilePendingRecreateClient) CreateOrgUnit(ctx context.Context, payload service.WorksmobileOrgUnitPayload) error {
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) UpsertOrgUnit(ctx context.Context, payload service.WorksmobileOrgUnitPayload, matchLocalPart string) error {
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) DeleteOrgUnit(ctx context.Context, orgUnitID string) error {
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) CreateUser(ctx context.Context, payload service.WorksmobileUserPayload) error {
if f.createErr != nil {
return f.createErr
}
f.createdUsers = append(f.createdUsers, payload)
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) UpsertUser(ctx context.Context, payload service.WorksmobileUserPayload) error {
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) AddUserAliasEmail(ctx context.Context, userID string, email string) error {
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) ResetUserPassword(ctx context.Context, userID string, password string) error {
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) DeleteUser(ctx context.Context, userID string) error {
f.deletedUsers = append(f.deletedUsers, userID)
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) PatchUser(ctx context.Context, identifier string, payload service.WorksmobileUserPatchPayload) error {
f.patchedUsers = append(f.patchedUsers, fakeWorksmobilePendingRecreatePatch{identifier: identifier, payload: payload})
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) SetUserActive(ctx context.Context, userID string, active bool) error {
return nil
}
func (f *fakeWorksmobilePendingRecreateClient) ListUsers(ctx context.Context) ([]service.WorksmobileRemoteUser, error) {
return nil, nil
}
func (f *fakeWorksmobilePendingRecreateClient) ListGroups(ctx context.Context) ([]service.WorksmobileRemoteGroup, error) {
return nil, nil
}
func (f *fakeWorksmobilePendingRecreateClient) UndeleteUser(ctx context.Context, userID string) error {
f.undeletedUsers = append(f.undeletedUsers, userID)
return nil
}
type fakeHanmacWorksmobileUserStore struct {
users map[string]domain.User
saved []domain.User
}
func (f *fakeHanmacWorksmobileUserStore) FindByEmail(ctx context.Context, email string) (domain.User, bool, error) {
if f.users == nil {
return domain.User{}, false, nil
}
user, ok := f.users[strings.ToLower(strings.TrimSpace(email))]
return user, ok, nil
}
func (f *fakeHanmacWorksmobileUserStore) Save(ctx context.Context, user *domain.User) (bool, error) {
created := true
if f.users == nil {
f.users = map[string]domain.User{}
} else if _, ok := f.users[strings.ToLower(strings.TrimSpace(user.Email))]; ok {
created = false
}
f.users[strings.ToLower(strings.TrimSpace(user.Email))] = *user
f.saved = append(f.saved, *user)
return created, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -395,6 +395,16 @@ func main() {
relyingPartyHandler := handler.NewRelyingPartyHandler(relyingPartyService, kratosAdminService)
userHandler := handler.NewUserHandler(kratosAdminService, oryAdminProvider, tenantService, ketoService, ketoOutboxRepo, userRepo, userGroupRepo, auditRepo)
userHandler.UserProjectionRepo = userProjectionRepo
userHandler.IdentityCache = redisService
go func() {
startedAt := time.Now()
count, err := userHandler.WarmIdentityMirror(context.Background())
if err != nil {
slog.Warn("Identity mirror warmup failed", "error", err, "latency", time.Since(startedAt).String())
return
}
slog.Info("Identity mirror warmup completed", "identities", count, "latency", time.Since(startedAt).String())
}()
tenantHandler.SetWorksmobileSyncer(worksmobileService)
userHandler.SetWorksmobileSyncer(worksmobileService)
worksmobileHandler := handler.NewWorksmobileHandler(worksmobileService)

View File

@@ -5,6 +5,7 @@ import "time"
type IdentityCacheStatus struct {
Status string `json:"status"`
RedisReady bool `json:"redisReady"`
MirrorVersion string `json:"mirrorVersion,omitempty"`
ObservedCount int64 `json:"observedCount"`
KeyCount int64 `json:"keyCount"`
LastRefreshedAt *time.Time `json:"lastRefreshedAt,omitempty"`

View File

@@ -307,13 +307,6 @@ func (h *AdminHandler) GetOrySSOTSystemStatus(c *fiber.Ctx) error {
if !requireSuperAdminProfile(c) {
return nil
}
if h == nil || h.UserProjectionRepo == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "user projection service unavailable"})
}
projectionStatus, err := h.UserProjectionRepo.GetStatus(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
cacheStatus := domain.IdentityCacheStatus{
Status: "unavailable",
@@ -321,6 +314,7 @@ func (h *AdminHandler) GetOrySSOTSystemStatus(c *fiber.Ctx) error {
LastError: "identity cache service unavailable",
}
if h.IdentityCache != nil {
var err error
cacheStatus, err = h.IdentityCache.GetIdentityCacheStatus(c.Context())
if err != nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": err.Error()})
@@ -328,7 +322,6 @@ func (h *AdminHandler) GetOrySSOTSystemStatus(c *fiber.Ctx) error {
}
return c.JSON(fiber.Map{
"userProjection": projectionStatus,
"identityCache": cacheStatus,
})
}

View File

@@ -209,7 +209,7 @@ func TestAdminHandler_UserProjectionStatusReturnsProjectionStateForSuperAdmin(t
require.Equal(t, int64(152), body.ProjectedUsers)
}
func TestAdminHandler_GetOrySSOTSystemStatusReturnsProjectionAndIdentityCache(t *testing.T) {
func TestAdminHandler_GetOrySSOTSystemStatusReturnsIdentityCacheOnly(t *testing.T) {
syncedAt := time.Date(2026, 5, 11, 3, 0, 0, 0, time.UTC)
cache := &fakeIdentityCacheAdmin{
status: domain.IdentityCacheStatus{
@@ -222,15 +222,6 @@ func TestAdminHandler_GetOrySSOTSystemStatusReturnsProjectionAndIdentityCache(t
},
}
h := &AdminHandler{
UserProjectionRepo: &fakeAdminUserProjectionRepo{
status: domain.UserProjectionStatus{
Name: domain.UserProjectionNameKratos,
Status: domain.UserProjectionStatusReady,
Ready: true,
LastSyncedAt: &syncedAt,
ProjectedUsers: 152,
},
},
IdentityCache: cache,
}
app := fiber.New()
@@ -246,11 +237,11 @@ func TestAdminHandler_GetOrySSOTSystemStatusReturnsProjectionAndIdentityCache(t
require.Equal(t, http.StatusOK, resp.StatusCode)
var body struct {
UserProjection domain.UserProjectionStatus `json:"userProjection"`
UserProjection *domain.UserProjectionStatus `json:"userProjection,omitempty"`
IdentityCache domain.IdentityCacheStatus `json:"identityCache"`
}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Equal(t, int64(152), body.UserProjection.ProjectedUsers)
require.Nil(t, body.UserProjection)
require.True(t, body.IdentityCache.RedisReady)
require.Equal(t, int64(151), body.IdentityCache.ObservedCount)
require.Equal(t, int64(153), body.IdentityCache.KeyCount)

View File

@@ -24,7 +24,6 @@ import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
// OryProviderAPI defines the subset of Ory Provider used by UserHandler
@@ -44,6 +43,7 @@ type UserHandler struct {
UserProjectionRepo repository.UserProjectionRepository
UserGroupRepo repository.UserGroupRepository
AuditRepo domain.AuditRepository
IdentityCache domain.RedisRepository
Worksmobile service.WorksmobileSyncer
}
@@ -589,6 +589,24 @@ func profileTenantAccessKeys(profile *domain.UserProfileResponse) map[string]boo
return allowed
}
func identityMirrorKey(identityID string) string {
return "identity:mirror:" + strings.TrimSpace(identityID)
}
type identityMirrorLister interface {
ListIdentityMirrors(ctx context.Context) ([]service.KratosIdentity, error)
}
type identityMirrorStatusReader interface {
GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error)
}
type identityMirrorFlusher interface {
FlushIdentityCache(ctx context.Context) (domain.IdentityCacheFlushResult, error)
}
const identityMirrorVersion = "kratos-full-pagination-v1"
func profileCanAccessTenant(profile *domain.UserProfileResponse, tenantID, tenantSlug string) bool {
allowed := profileTenantAccessKeys(profile)
if id := strings.ToLower(strings.TrimSpace(tenantID)); id != "" && allowed[id] {
@@ -654,6 +672,26 @@ func kratosIdentityCursorKey(identity service.KratosIdentity) (time.Time, string
return timestamp, identity.ID
}
func identityMatchesSearch(identity service.KratosIdentity, searchLower string) bool {
if searchLower == "" {
return true
}
if strings.Contains(strings.ToLower(identity.ID), searchLower) {
return true
}
if strings.Contains(strings.ToLower(extractTraitString(identity.Traits, "email")), searchLower) {
return true
}
if strings.Contains(strings.ToLower(extractTraitString(identity.Traits, "name")), searchLower) {
return true
}
rawTraits, err := json.Marshal(identity.Traits)
if err != nil {
return false
}
return strings.Contains(strings.ToLower(string(rawTraits)), searchLower)
}
func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
// [New] Get requester profile from middleware
var requesterRole string
@@ -745,10 +783,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
}
}
if h.UserRepo != nil {
var tenantIDs []string
if tenantSlug != "" {
if targetTenantID == "" {
if tenantSlug != "" && targetTenantID == "" {
return c.JSON(userListResponse{
Items: []userSummary{},
Limit: limit,
@@ -757,66 +792,27 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
Cursor: cursorRaw,
})
}
if requesterRole != domain.RoleSuperAdmin && !manageableSlugs[targetTenantID] && !manageableSlugs[strings.ToLower(tenantSlug)] {
return c.JSON(userListResponse{
Items: []userSummary{},
Limit: limit,
Offset: offset,
Total: 0,
Cursor: cursorRaw,
})
}
tenantIDs = append(tenantIDs, targetTenantID)
} else if requesterRole != domain.RoleSuperAdmin {
for key := range manageableSlugs {
if _, err := uuid.Parse(key); err == nil {
tenantIDs = append(tenantIDs, key)
}
}
if len(tenantIDs) == 0 {
return c.JSON(userListResponse{
Items: []userSummary{},
Limit: limit,
Offset: offset,
Total: 0,
Cursor: cursorRaw,
})
}
}
users, total, nextCursor, err := h.UserRepo.List(c.Context(), offset, limit, search, tenantIDs, cursorRaw)
if requesterRole != domain.RoleSuperAdmin && tenantSlug != "" && !manageableSlugs[targetTenantID] && !manageableSlugs[strings.ToLower(tenantSlug)] {
return c.JSON(userListResponse{
Items: []userSummary{},
Limit: limit,
Offset: offset,
Total: 0,
Cursor: cursorRaw,
})
}
identities, err := h.listIdentitiesFromMirrorOrKratos(c.Context())
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, "failed to list users")
}
items := make([]userSummary, 0, len(users))
for _, user := range users {
items = append(items, h.mapLocalUserSummary(c.Context(), user))
}
if cursorRaw != "" {
offset = 0
}
return c.JSON(userListResponse{
Items: items,
Limit: limit,
Offset: offset,
Total: total,
Cursor: cursorRaw,
NextCursor: nextCursor,
})
slog.Warn("Identity mirror unavailable for user list", "error", err)
return errorJSON(c, fiber.StatusServiceUnavailable, "identity mirror unavailable")
}
if h.KratosAdmin == nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
}
identities, err := h.KratosAdmin.ListIdentities(c.Context())
if err == nil {
filtered := make([]service.KratosIdentity, 0, len(identities))
searchLower := strings.ToLower(search)
for _, identity := range identities {
email := strings.ToLower(extractTraitString(identity.Traits, "email"))
name := strings.ToLower(extractTraitString(identity.Traits, "name"))
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
// Tenant Admin & Member filtering
@@ -835,15 +831,9 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
}
}
// Search filtering
if search != "" {
matchesSearch := strings.Contains(email, searchLower) ||
strings.Contains(name, searchLower)
if !matchesSearch {
if !identityMatchesSearch(identity, searchLower) {
continue
}
}
filtered = append(filtered, identity)
}
@@ -875,19 +865,6 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
items = append(items, summary)
}
// [Lazy Sync] Asynchronously update local DB with fresh data from Kratos
// This ensures that member counts (which use local DB) eventually match reality
if h.UserRepo != nil {
go func(ids []service.KratosIdentity) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
for _, identity := range ids {
localUser := h.mapToLocalUser(identity)
_ = h.UserRepo.Update(ctx, localUser)
}
}(filtered)
}
return c.JSON(userListResponse{
Items: items,
Limit: limit,
@@ -896,10 +873,6 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
Cursor: cursorRaw,
NextCursor: nextCursor,
})
}
slog.Warn("Kratos unavailable for user list", "error", err)
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider unavailable")
}
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
@@ -912,27 +885,31 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusBadRequest, "user id is required")
}
if identity := h.getIdentityFromMirror(userID); identity != nil {
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
if requester != nil && requester.Role != domain.RoleSuperAdmin {
allowedKeys := profileTenantAccessKeys(requester)
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowedKeys) {
return errorJSON(c, fiber.StatusForbidden, "forbidden: access to user in another tenant denied")
}
}
return c.JSON(h.mapIdentitySummary(c.Context(), *identity))
}
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
if err != nil || identity == nil {
// [FIX] Support fixed UUID lookup fallback
id, searchErr := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), userID)
if searchErr == nil && id != "" {
if cached := h.getIdentityFromMirror(id); cached != nil {
identity = cached
err = nil
}
}
if searchErr == nil && id != "" && (err != nil || identity == nil) {
identity, err = h.KratosAdmin.GetIdentity(c.Context(), id)
}
if err != nil || identity == nil {
// Second Fallback: By Email from local DB
if h.UserRepo != nil {
local, _ := h.UserRepo.FindByID(c.Context(), userID)
if local != nil && local.Email != "" {
id, _ = h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), local.Email)
if id != "" {
identity, err = h.KratosAdmin.GetIdentity(c.Context(), id)
}
}
}
}
if err != nil || identity == nil {
if identity == nil {
return errorJSON(c, fiber.StatusNotFound, "user not found")
@@ -940,6 +917,7 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
}
h.storeIdentityMirror(*identity)
// [New] Check access scope
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
@@ -953,6 +931,149 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
return c.JSON(h.mapIdentitySummary(c.Context(), *identity))
}
func (h *UserHandler) getIdentityFromMirror(identityID string) *service.KratosIdentity {
if h == nil || h.IdentityCache == nil {
return nil
}
raw, err := h.IdentityCache.Get(identityMirrorKey(identityID))
if err != nil || strings.TrimSpace(raw) == "" {
return nil
}
var identity service.KratosIdentity
if err := json.Unmarshal([]byte(raw), &identity); err != nil {
return nil
}
if strings.TrimSpace(identity.ID) == "" {
return nil
}
return &identity
}
func (h *UserHandler) listIdentitiesFromMirrorOrKratos(ctx context.Context) ([]service.KratosIdentity, error) {
if h != nil && h.IdentityCache != nil {
if lister, ok := h.IdentityCache.(identityMirrorLister); ok {
identities, err := lister.ListIdentityMirrors(ctx)
if err != nil {
return nil, err
}
if h.identityMirrorReady(ctx, len(identities)) {
return identities, nil
}
}
}
if h == nil || h.KratosAdmin == nil {
return nil, errors.New("identity mirror is empty and kratos admin service is unavailable")
}
return h.rebuildIdentityMirror(ctx)
}
func (h *UserHandler) WarmIdentityMirror(ctx context.Context) (int, error) {
identities, err := h.rebuildIdentityMirror(ctx)
if err != nil {
return 0, err
}
return len(identities), nil
}
func (h *UserHandler) rebuildIdentityMirror(ctx context.Context) ([]service.KratosIdentity, error) {
if h == nil || h.KratosAdmin == nil {
return nil, errors.New("kratos admin service is unavailable")
}
identities, err := h.KratosAdmin.ListIdentities(ctx)
if err != nil {
return nil, err
}
h.flushIdentityMirror(ctx)
for _, identity := range identities {
h.storeIdentityMirror(identity)
}
h.markIdentityMirrorReady(len(identities))
return identities, nil
}
func (h *UserHandler) identityMirrorReady(ctx context.Context, identityCount int) bool {
if h == nil || h.IdentityCache == nil || identityCount == 0 {
return false
}
reader, ok := h.IdentityCache.(identityMirrorStatusReader)
if !ok {
return false
}
status, err := reader.GetIdentityCacheStatus(ctx)
if err != nil {
return false
}
return status.RedisReady &&
status.Status == "ready" &&
status.MirrorVersion == identityMirrorVersion &&
status.ObservedCount == int64(identityCount)
}
func (h *UserHandler) flushIdentityMirror(ctx context.Context) {
if h == nil || h.IdentityCache == nil {
return
}
flusher, ok := h.IdentityCache.(identityMirrorFlusher)
if !ok {
return
}
_, _ = flusher.FlushIdentityCache(ctx)
}
func (h *UserHandler) markIdentityMirrorReady(identityCount int) {
if h == nil || h.IdentityCache == nil {
return
}
now := time.Now().UTC()
status := domain.IdentityCacheStatus{
Status: "ready",
RedisReady: true,
MirrorVersion: identityMirrorVersion,
ObservedCount: int64(identityCount),
LastRefreshedAt: &now,
UpdatedAt: &now,
}
raw, err := json.Marshal(status)
if err != nil {
return
}
_ = h.IdentityCache.Set("identity:mirror:state", string(raw), 0)
}
func (h *UserHandler) invalidateIdentityMirrorState() {
if h == nil || h.IdentityCache == nil {
return
}
_ = h.IdentityCache.Delete("identity:mirror:state")
}
func (h *UserHandler) storeIdentityMirror(identity service.KratosIdentity) {
if h == nil || h.IdentityCache == nil || strings.TrimSpace(identity.ID) == "" {
return
}
raw, err := json.Marshal(identity)
if err != nil {
return
}
_ = h.IdentityCache.Set(identityMirrorKey(identity.ID), string(raw), 0)
}
func (h *UserHandler) updateIdentityMirrorEntry(identity service.KratosIdentity) {
h.storeIdentityMirror(identity)
h.invalidateIdentityMirrorState()
}
func (h *UserHandler) deleteIdentityMirrorEntry(identityID string) {
if h == nil || h.IdentityCache == nil {
return
}
identityID = strings.TrimSpace(identityID)
if identityID != "" {
_ = h.IdentityCache.Delete(identityMirrorKey(identityID))
}
h.invalidateIdentityMirrorState()
}
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
if h.OryProvider == nil || h.KratosAdmin == nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
@@ -1171,8 +1292,10 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
if identity == nil {
h.invalidateIdentityMirrorState()
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"id": identityID, "initialPassword": generatedPassword})
}
h.updateIdentityMirrorEntry(*identity)
// [New] Local DB Sync - Ensure user exists in read-model
if h.UserRepo != nil {
@@ -1672,6 +1795,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
}
} else {
resultStatus = "created"
h.invalidateIdentityMirrorState()
slog.Info("BulkCreate: New identity created", "email", userEmail, "identityID", identityID)
}
}
@@ -2160,6 +2284,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
continue
}
h.updateIdentityMirrorEntry(*updated)
// Sync to local DB
if h.UserRepo != nil {
@@ -2267,6 +2392,7 @@ func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
continue
}
h.deleteIdentityMirrorEntry(id)
if h.Worksmobile != nil {
localUser := h.mapToLocalUser(*identity)
if err := h.Worksmobile.EnqueueUserDeleteIfInScope(c.Context(), *localUser); err != nil {
@@ -2635,6 +2761,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
}
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
h.updateIdentityMirrorEntry(*updated)
// [New] Local DB Sync - Sync synchronously to ensure immediate consistency for the caller
if h.UserRepo != nil {
@@ -2807,6 +2934,10 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
}
h.deleteIdentityMirrorEntry(userID)
if actualKratosID != userID {
h.deleteIdentityMirrorEntry(actualKratosID)
}
slog.Info("[UserHandler] Successfully deleted Kratos identity", "userID", userID, "actualKratosID", actualKratosID)
if h.Worksmobile != nil && identity != nil {
@@ -3003,16 +3134,6 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
traits := identity.Traits
role := roleFromTraits(traits)
// [FIX] Prioritize Local DB ID (the fixed UUID from user)
finalID := identity.ID
email := extractTraitString(traits, "email")
if h.UserRepo != nil && email != "" {
// 1. Try finding by email first as it's a strong identifier
if local, err := h.UserRepo.FindByEmail(ctx, email); err == nil && local != nil {
finalID = local.ID
}
}
tenantID := extractTraitString(traits, "tenant_id")
tenantSlug := ""
var tenantSummary *domain.Tenant
@@ -3038,7 +3159,7 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
}
summary := userSummary{
ID: finalID,
ID: identity.ID,
Email: extractTraitString(traits, "email"),
LoginID: resolvePasswordLoginID(traits),
CustomLoginIDs: customLoginIDs,

View File

@@ -0,0 +1,46 @@
package handler
import (
"baron-sso-backend/internal/service"
"context"
"os"
"strconv"
"testing"
"time"
)
func TestUserHandler_LiveWarmIdentityMirrorLatency(t *testing.T) {
if os.Getenv("BARON_LIVE_IDENTITY_MIRROR_TEST") != "1" {
t.Skip("set BARON_LIVE_IDENTITY_MIRROR_TEST=1 to run against local Kratos and Redis")
}
redisService, err := service.NewRedisService()
if err != nil {
t.Fatalf("connect redis: %v", err)
}
kratosAdmin := service.NewKratosAdminService()
handler := &UserHandler{
KratosAdmin: kratosAdmin,
IdentityCache: redisService,
}
startedAt := time.Now()
count, err := handler.WarmIdentityMirror(context.Background())
elapsed := time.Since(startedAt)
if err != nil {
t.Fatalf("warm identity mirror: %v", err)
}
maxMillis := int64(2000)
if raw := os.Getenv("BARON_LIVE_IDENTITY_MIRROR_MAX_MS"); raw != "" {
parsed, err := strconv.ParseInt(raw, 10, 64)
if err != nil || parsed <= 0 {
t.Fatalf("invalid BARON_LIVE_IDENTITY_MIRROR_MAX_MS=%q", raw)
}
maxMillis = parsed
}
t.Logf("identity mirror warmup identities=%d elapsed=%s max=%dms", count, elapsed, maxMillis)
if elapsed > time.Duration(maxMillis)*time.Millisecond {
t.Fatalf("identity mirror warmup took %s, over %dms", elapsed, maxMillis)
}
}

View File

@@ -981,15 +981,88 @@ func TestUserHandler_BulkCreateUsers_UsesEmailDomainTenantAsPrimaryWhenExplicitT
mockOry.AssertExpectations(t)
}
func TestUserHandler_ListUsersUsesLocalProjectionWhenKratosFails(t *testing.T) {
type identityMirrorRedisStub struct {
mockRedisRepo
}
func (s *identityMirrorRedisStub) ListIdentityMirrors(ctx context.Context) ([]service.KratosIdentity, error) {
identities := make([]service.KratosIdentity, 0, len(s.data))
for key, raw := range s.data {
if !strings.HasPrefix(key, "identity:mirror:") || key == "identity:mirror:state" {
continue
}
var identity service.KratosIdentity
if err := json.Unmarshal([]byte(raw), &identity); err != nil {
continue
}
if strings.TrimSpace(identity.ID) == "" {
continue
}
identities = append(identities, identity)
}
return identities, nil
}
func (s *identityMirrorRedisStub) GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error) {
raw := s.data["identity:mirror:state"]
if strings.TrimSpace(raw) == "" {
return domain.IdentityCacheStatus{RedisReady: true, Status: "empty"}, nil
}
var status domain.IdentityCacheStatus
if err := json.Unmarshal([]byte(raw), &status); err != nil {
return domain.IdentityCacheStatus{}, err
}
return status, nil
}
func (s *identityMirrorRedisStub) FlushIdentityCache(ctx context.Context) (domain.IdentityCacheFlushResult, error) {
var deleted int64
for key := range s.data {
if strings.HasPrefix(key, "identity:mirror:") || strings.HasPrefix(key, "identity:index:") {
delete(s.data, key)
deleted++
}
}
return domain.IdentityCacheFlushResult{
Status: "success",
FlushedKeys: deleted,
UpdatedAt: time.Now().UTC(),
}, nil
}
func TestUserHandler_ListUsersUsesIdentityMirrorAndDoesNotUseUserRepo(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockRepo := new(MockUserRepoForHandler)
createdAt := time.Date(2026, 6, 8, 6, 30, 0, 0, time.UTC)
mirrorIdentity := service.KratosIdentity{
ID: "mirror-user-1",
State: "active",
CreatedAt: createdAt,
UpdatedAt: createdAt,
Traits: map[string]any{
"email": "mirror1@example.com",
"name": "Mirror One",
},
}
rawMirrorIdentity, err := json.Marshal(mirrorIdentity)
require.NoError(t, err)
state := domain.IdentityCacheStatus{
Status: "ready",
RedisReady: true,
MirrorVersion: identityMirrorVersion,
ObservedCount: 1,
}
rawState, err := json.Marshal(state)
require.NoError(t, err)
h := &UserHandler{
KratosAdmin: mockKratos,
UserRepo: mockRepo,
IdentityCache: &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{
identityMirrorKey(mirrorIdentity.ID): string(rawMirrorIdentity),
"identity:mirror:state": string(rawState),
}}},
}
app.Use(func(c *fiber.Ctx) error {
@@ -1000,19 +1073,6 @@ func TestUserHandler_ListUsersUsesLocalProjectionWhenKratosFails(t *testing.T) {
})
app.Get("/users", h.ListUsers)
mockKratos.On("ListIdentities", mock.Anything).Return([]service.KratosIdentity{}, errors.New("kratos down")).Maybe()
mockRepo.On("List", mock.Anything, 0, 10, "", []string(nil), "").Return([]domain.User{
{
ID: "local-user-1",
Email: "local1@example.com",
Name: "Local One",
Role: domain.RoleUser,
Status: domain.UserStatusActive,
CreatedAt: createdAt,
UpdatedAt: createdAt,
},
}, int64(1), "", nil)
req := httptest.NewRequest("GET", "/users?limit=10&offset=0", nil)
resp, err := app.Test(req)
@@ -1023,19 +1083,21 @@ func TestUserHandler_ListUsersUsesLocalProjectionWhenKratosFails(t *testing.T) {
require.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
require.Equal(t, int64(1), res.Total)
require.Len(t, res.Items, 1)
require.Equal(t, "local1@example.com", res.Items[0].Email)
mockRepo.AssertExpectations(t)
require.Equal(t, "mirror-user-1", res.Items[0].ID)
require.Equal(t, "mirror1@example.com", res.Items[0].Email)
mockRepo.AssertNotCalled(t, "List", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
mockKratos.AssertNotCalled(t, "ListIdentities", mock.Anything)
}
func TestUserHandler_ListUsersUsesLocalProjectionTotalBeyondKratosPageLimit(t *testing.T) {
func TestUserHandler_ListUsersWarmsIdentityMirrorFromKratosWhenMirrorEmpty(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockRepo := new(MockUserRepoForHandler)
redis := &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{}}}
createdAt := time.Date(2026, 6, 8, 6, 40, 0, 0, time.UTC)
h := &UserHandler{
KratosAdmin: mockKratos,
UserRepo: mockRepo,
IdentityCache: redis,
}
app.Use(func(c *fiber.Ctx) error {
@@ -1046,27 +1108,11 @@ func TestUserHandler_ListUsersUsesLocalProjectionTotalBeyondKratosPageLimit(t *t
})
app.Get("/users", h.ListUsers)
kratosIdentities := make([]service.KratosIdentity, 250)
for i := range kratosIdentities {
kratosIdentities[i] = service.KratosIdentity{
ID: "kratos-user",
State: "active",
CreatedAt: createdAt.Add(-time.Duration(i) * time.Second),
Traits: map[string]any{"email": "kratos@example.com", "name": "Kratos"},
kratosIdentities := []service.KratosIdentity{
{ID: "kratos-user-1", State: "active", CreatedAt: createdAt, UpdatedAt: createdAt, Traits: map[string]any{"email": "kratos1@example.com", "name": "Kratos One"}},
{ID: "kratos-user-2", State: "active", CreatedAt: createdAt.Add(-time.Minute), UpdatedAt: createdAt, Traits: map[string]any{"email": "kratos2@example.com", "name": "Kratos Two"}},
}
}
mockKratos.On("ListIdentities", mock.Anything).Return(kratosIdentities, nil).Maybe()
mockRepo.On("List", mock.Anything, 0, 50, "", []string(nil), "").Return([]domain.User{
{
ID: "local-user-1",
Email: "local1@example.com",
Name: "Local One",
Role: domain.RoleUser,
Status: domain.UserStatusActive,
CreatedAt: createdAt,
UpdatedAt: createdAt,
},
}, int64(2114), "next-local-cursor", nil)
mockKratos.On("ListIdentities", mock.Anything).Return(kratosIdentities, nil).Once()
req := httptest.NewRequest("GET", "/users?limit=50&offset=0", nil)
resp, err := app.Test(req)
@@ -1076,11 +1122,162 @@ func TestUserHandler_ListUsersUsesLocalProjectionTotalBeyondKratosPageLimit(t *t
var res userListResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
require.Equal(t, int64(2114), res.Total)
require.Len(t, res.Items, 1)
require.Equal(t, "local1@example.com", res.Items[0].Email)
require.Equal(t, "next-local-cursor", res.NextCursor)
mockRepo.AssertExpectations(t)
require.Equal(t, int64(2), res.Total)
require.Len(t, res.Items, 2)
require.Equal(t, "kratos-user-1", res.Items[0].ID)
require.NotEmpty(t, redis.data[identityMirrorKey("kratos-user-1")])
require.NotEmpty(t, redis.data[identityMirrorKey("kratos-user-2")])
var status domain.IdentityCacheStatus
require.NoError(t, json.Unmarshal([]byte(redis.data["identity:mirror:state"]), &status))
require.Equal(t, "ready", status.Status)
require.Equal(t, identityMirrorVersion, status.MirrorVersion)
require.Equal(t, int64(2), status.ObservedCount)
mockKratos.AssertExpectations(t)
}
func TestUserHandler_WarmIdentityMirrorRebuildsRedisFromKratos(t *testing.T) {
mockKratos := new(MockKratosAdmin)
redis := &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{
identityMirrorKey("stale-user"): `{"id":"stale-user"}`,
}}}
createdAt := time.Date(2026, 6, 12, 18, 30, 0, 0, time.UTC)
identities := []service.KratosIdentity{
{ID: "kratos-user-1", State: "active", CreatedAt: createdAt, UpdatedAt: createdAt, Traits: map[string]any{"email": "kratos1@example.com"}},
{ID: "kratos-user-2", State: "active", CreatedAt: createdAt.Add(-time.Second), UpdatedAt: createdAt, Traits: map[string]any{"email": "kratos2@example.com"}},
}
mockKratos.On("ListIdentities", mock.Anything).Return(identities, nil).Once()
h := &UserHandler{
KratosAdmin: mockKratos,
IdentityCache: redis,
}
count, err := h.WarmIdentityMirror(context.Background())
require.NoError(t, err)
require.Equal(t, 2, count)
require.Empty(t, redis.data[identityMirrorKey("stale-user")])
require.NotEmpty(t, redis.data[identityMirrorKey("kratos-user-1")])
require.NotEmpty(t, redis.data[identityMirrorKey("kratos-user-2")])
var status domain.IdentityCacheStatus
require.NoError(t, json.Unmarshal([]byte(redis.data["identity:mirror:state"]), &status))
require.Equal(t, "ready", status.Status)
require.Equal(t, identityMirrorVersion, status.MirrorVersion)
require.Equal(t, int64(2), status.ObservedCount)
mockKratos.AssertExpectations(t)
}
func TestUserHandler_ListUsersRebuildsLegacyReadyMirrorWithoutVersion(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
createdAt := time.Date(2026, 6, 8, 6, 55, 0, 0, time.UTC)
legacyIdentity := service.KratosIdentity{
ID: "legacy-partial-user",
State: "active",
CreatedAt: createdAt,
UpdatedAt: createdAt,
Traits: map[string]any{
"email": "legacy@example.com",
"name": "Legacy Partial",
},
}
rawLegacyIdentity, err := json.Marshal(legacyIdentity)
require.NoError(t, err)
legacyState := domain.IdentityCacheStatus{
Status: "ready",
RedisReady: true,
ObservedCount: 1,
}
rawLegacyState, err := json.Marshal(legacyState)
require.NoError(t, err)
redis := &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{
identityMirrorKey(legacyIdentity.ID): string(rawLegacyIdentity),
"identity:mirror:state": string(rawLegacyState),
}}}
kratosIdentities := []service.KratosIdentity{
{ID: "kratos-user-1", State: "active", CreatedAt: createdAt, UpdatedAt: createdAt, Traits: map[string]any{"email": "kratos1@example.com", "name": "Kratos One"}},
{ID: "kratos-user-2", State: "active", CreatedAt: createdAt.Add(-time.Minute), UpdatedAt: createdAt, Traits: map[string]any{"email": "kratos2@example.com", "name": "Kratos Two"}},
}
mockKratos.On("ListIdentities", mock.Anything).Return(kratosIdentities, nil).Once()
h := &UserHandler{
KratosAdmin: mockKratos,
IdentityCache: redis,
}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
Role: domain.RoleSuperAdmin,
})
return c.Next()
})
app.Get("/users", h.ListUsers)
req := httptest.NewRequest("GET", "/users?limit=50&offset=0", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var res userListResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
require.Equal(t, int64(2), res.Total)
var status domain.IdentityCacheStatus
require.NoError(t, json.Unmarshal([]byte(redis.data["identity:mirror:state"]), &status))
require.Equal(t, identityMirrorVersion, status.MirrorVersion)
mockKratos.AssertExpectations(t)
}
func TestUserHandler_ListUsersRebuildsPartialMirrorFromKratos(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
createdAt := time.Date(2026, 6, 8, 6, 50, 0, 0, time.UTC)
partialIdentity := service.KratosIdentity{
ID: "partial-user",
State: "active",
CreatedAt: createdAt,
UpdatedAt: createdAt,
Traits: map[string]any{
"email": "partial@example.com",
"name": "Partial",
},
}
rawPartialIdentity, err := json.Marshal(partialIdentity)
require.NoError(t, err)
redis := &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{
identityMirrorKey(partialIdentity.ID): string(rawPartialIdentity),
}}}
kratosIdentities := []service.KratosIdentity{
{ID: "kratos-user-1", State: "active", CreatedAt: createdAt, UpdatedAt: createdAt, Traits: map[string]any{"email": "kratos1@example.com", "name": "Kratos One"}},
{ID: "kratos-user-2", State: "active", CreatedAt: createdAt.Add(-time.Minute), UpdatedAt: createdAt, Traits: map[string]any{"email": "kratos2@example.com", "name": "Kratos Two"}},
}
mockKratos.On("ListIdentities", mock.Anything).Return(kratosIdentities, nil).Once()
h := &UserHandler{
KratosAdmin: mockKratos,
IdentityCache: redis,
}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
Role: domain.RoleSuperAdmin,
})
return c.Next()
})
app.Get("/users", h.ListUsers)
req := httptest.NewRequest("GET", "/users?limit=50&offset=0", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var res userListResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
require.Equal(t, int64(2), res.Total)
require.Empty(t, redis.data[identityMirrorKey("partial-user")])
require.NotEmpty(t, redis.data[identityMirrorKey("kratos-user-1")])
require.NotEmpty(t, redis.data[identityMirrorKey("kratos-user-2")])
mockKratos.AssertExpectations(t)
}
func TestUserHandler_ListUsersReturnsNextCursorWhenMoreRowsExist(t *testing.T) {
@@ -1117,6 +1314,86 @@ func TestUserHandler_ListUsersReturnsNextCursorWhenMoreRowsExist(t *testing.T) {
require.Equal(t, int64(3), res.Total)
}
func TestUserHandler_GetUserUsesIdentityMirrorBeforeKratos(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
createdAt := time.Date(2026, 6, 12, 8, 20, 0, 0, time.UTC)
userID := "2b7fd276-b25f-45ef-b691-ea9d72e701e1"
identity := service.KratosIdentity{
ID: userID,
State: "active",
CreatedAt: createdAt,
UpdatedAt: createdAt,
Traits: map[string]any{
"email": "mirror-user@example.com",
"name": "Mirror User",
},
}
rawIdentity, err := json.Marshal(identity)
require.NoError(t, err)
redis := &mockRedisRepo{data: map[string]string{
identityMirrorKey(userID): string(rawIdentity),
}}
h := &UserHandler{
KratosAdmin: mockKratos,
IdentityCache: redis,
}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
Role: domain.RoleSuperAdmin,
})
return c.Next()
})
app.Get("/users/:id", h.GetUser)
req := httptest.NewRequest(http.MethodGet, "/users/"+userID, nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var got userSummary
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
require.Equal(t, userID, got.ID)
require.Equal(t, "mirror-user@example.com", got.Email)
require.Equal(t, "Mirror User", got.Name)
mockKratos.AssertNotCalled(t, "GetIdentity", mock.Anything, mock.Anything)
mockKratos.AssertNotCalled(t, "FindIdentityIDByIdentifier", mock.Anything, mock.Anything)
}
func TestUserHandler_UpdateIdentityMirrorEntryInvalidatesReadyState(t *testing.T) {
redis := &mockRedisRepo{data: map[string]string{
"identity:mirror:state": `{"status":"ready","redisReady":true,"observedCount":1}`,
}}
h := &UserHandler{IdentityCache: redis}
identity := service.KratosIdentity{
ID: "user-1",
Traits: map[string]any{
"email": "user1@example.com",
},
}
h.updateIdentityMirrorEntry(identity)
require.Empty(t, redis.data["identity:mirror:state"])
require.NotEmpty(t, redis.data[identityMirrorKey("user-1")])
}
func TestUserHandler_DeleteIdentityMirrorEntryInvalidatesReadyState(t *testing.T) {
redis := &mockRedisRepo{data: map[string]string{
"identity:mirror:state": `{"status":"ready","redisReady":true,"observedCount":1}`,
identityMirrorKey("u-1"): `{"id":"u-1"}`,
}}
h := &UserHandler{IdentityCache: redis}
h.deleteIdentityMirrorEntry("u-1")
require.Empty(t, redis.data["identity:mirror:state"])
require.Empty(t, redis.data[identityMirrorKey("u-1")])
}
func TestUserHandler_BulkCreateUsers_HanmacEmailPolicy(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)

View File

@@ -213,5 +213,19 @@ func worksmobileGuardError(c *fiber.Ctx, err error, operation string, attrs ...a
if strings.Contains(err.Error(), "hanmac-family root") {
return errorJSON(c, fiber.StatusNotFound, err.Error())
}
if worksmobileBadRequestError(err) {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
func worksmobileBadRequestError(err error) bool {
message := err.Error()
return strings.Contains(message, "target user tenant is excluded from Worksmobile sync") ||
strings.Contains(message, "target user is outside hanmac-family subtree") ||
strings.Contains(message, "target user has no tenant") ||
strings.Contains(message, "target user status is excluded from Worksmobile sync") ||
strings.Contains(message, "target tenant is excluded from Worksmobile sync") ||
strings.Contains(message, "target tenant is not a worksmobile orgunit tenant") ||
strings.Contains(message, "target orgunit is outside hanmac-family subtree")
}

View File

@@ -195,6 +195,19 @@ func TestWorksmobileHandlerLogsActionFailures(t *testing.T) {
require.Contains(t, logs.String(), "works user sync failed")
}
func TestWorksmobileHandlerReturnsBadRequestForOutOfScopeUserSync(t *testing.T) {
h := NewWorksmobileHandler(&fakeWorksmobileAdminService{
syncUserErr: errors.New("target user tenant is excluded from Worksmobile sync"),
})
app := fiber.New()
app.Post("/tenants/:tenantId/worksmobile/users/:userId/sync", h.SyncUser)
resp, err := app.Test(httptest.NewRequest("POST", "/tenants/hanmac-id/worksmobile/users/user-1/sync", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusBadRequest, resp.StatusCode)
}
type fakeWorksmobileAdminService struct {
overview service.WorksmobileTenantOverview
credentials []service.WorksmobileInitialPasswordCredential

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"os"
"strings"
"time"
"github.com/go-redis/redis/v8"
@@ -199,6 +200,39 @@ func (s *RedisService) FlushIdentityCache(ctx context.Context) (domain.IdentityC
}, nil
}
func (s *RedisService) ListIdentityMirrors(ctx context.Context) ([]KratosIdentity, error) {
if s == nil || s.Client == nil {
return nil, os.ErrInvalid
}
keys, err := s.identityCacheKeys(ctx)
if err != nil {
return nil, err
}
identities := make([]KratosIdentity, 0, len(keys))
for _, key := range keys {
if key == "identity:mirror:state" || !strings.HasPrefix(key, "identity:mirror:") {
continue
}
raw, err := s.Client.Get(ctx, key).Result()
if err == redis.Nil {
continue
}
if err != nil {
return nil, err
}
var identity KratosIdentity
if err := json.Unmarshal([]byte(raw), &identity); err != nil {
continue
}
if strings.TrimSpace(identity.ID) == "" {
continue
}
identities = append(identities, identity)
}
return identities, nil
}
func (s *RedisService) countIdentityCacheKeys(ctx context.Context) (int64, error) {
keys, err := s.identityCacheKeys(ctx)
if err != nil {

View File

@@ -434,6 +434,9 @@ func (c *WorksmobileHTTPClient) DeleteUser(ctx context.Context, userID string) e
if userID == "" {
return fmt.Errorf("worksmobile user id is required")
}
if c.directoryAuthConfigured() && strings.Contains(userID, "@") {
return c.sendDirectoryJSON(ctx, http.MethodDelete, "/v1.0/users/"+url.PathEscape(userID), nil)
}
remote, err := c.FindUser(ctx, userID)
if err != nil {
return err
@@ -450,6 +453,14 @@ func (c *WorksmobileHTTPClient) DeleteUser(ctx context.Context, userID string) e
return c.sendJSON(ctx, http.MethodDelete, "/scim/v2/Users/"+url.PathEscape(remote.ID), nil)
}
func (c *WorksmobileHTTPClient) ForceDeleteUser(ctx context.Context, userID string) error {
userID = strings.TrimSpace(userID)
if userID == "" {
return fmt.Errorf("worksmobile user id is required")
}
return c.sendDirectoryJSON(ctx, http.MethodDelete, "/v1.0/users/"+url.PathEscape(userID)+"/forcedelete", nil)
}
func (c *WorksmobileHTTPClient) SetUserActive(ctx context.Context, userID string, active bool) error {
userID = strings.TrimSpace(userID)
if userID == "" {
@@ -465,7 +476,18 @@ func (c *WorksmobileHTTPClient) SetUserActive(ctx context.Context, userID string
if remote == nil {
return nil
}
return c.sendJSON(ctx, http.MethodPatch, "/scim/v2/Users/"+url.PathEscape(remote.ID), map[string]any{
return c.SetSCIMUserActiveByID(ctx, remote.ID, active)
}
func (c *WorksmobileHTTPClient) SetSCIMUserActiveByID(ctx context.Context, scimID string, active bool) error {
scimID = strings.TrimSpace(scimID)
if scimID == "" {
return fmt.Errorf("worksmobile scim user id is required")
}
if strings.TrimSpace(c.SCIMToken) == "" {
return fmt.Errorf("worksmobile scim token is not configured")
}
return c.sendJSON(ctx, http.MethodPatch, "/scim/v2/Users/"+url.PathEscape(scimID), map[string]any{
"schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"},
"Operations": []map[string]any{
{
@@ -926,6 +948,7 @@ type WorksmobileRemoteUser struct {
PrimaryOrgUnitIsManager *bool `json:"primaryOrgUnitIsManager,omitempty"`
OrgUnitManagers map[string]*bool `json:"orgUnitManagers,omitempty"`
Organizations []WorksmobileUserOrganization `json:"organizations,omitempty"`
AccountStatus string `json:"accountStatus,omitempty"`
Active bool `json:"active"`
IsAwaiting bool `json:"isAwaiting"`
IsPending bool `json:"isPending"`
@@ -1010,12 +1033,21 @@ func worksmobileSCIMPreferredLanguage(locale string) string {
}
func parseWorksmobileRemoteUser(resource map[string]any) WorksmobileRemoteUser {
active := boolFromMap(resource, "active")
user := WorksmobileRemoteUser{
ID: stringFromMap(resource, "id"),
ExternalID: stringFromMap(resource, "externalId"),
UserName: stringFromMap(resource, "userName"),
DisplayName: stringFromMap(resource, "displayName"),
Active: boolFromMap(resource, "active"),
AccountStatus: normalizeWorksmobileAccountStatus(
firstStringFromMap(resource, "accountStatus", "status", "userStatus"),
active,
false,
false,
false,
false,
),
Active: active,
}
if emails, ok := resource["emails"].([]any); ok {
for _, raw := range emails {
@@ -1077,6 +1109,14 @@ func parseWorksmobileDirectoryUser(resource map[string]any) WorksmobileRemoteUse
user.IsPending = boolFromMap(resource, "isPending")
user.IsSuspended = boolFromMap(resource, "isSuspended")
user.IsDeleted = boolFromMap(resource, "isDeleted")
user.AccountStatus = normalizeWorksmobileAccountStatus(
firstStringFromMap(resource, "accountStatus", "status", "userStatus", "loginStatus"),
user.Active,
user.IsAwaiting,
user.IsPending,
user.IsSuspended,
user.IsDeleted,
)
primaryOrgUnit := parseWorksmobilePrimaryOrgUnitDetail(resource)
user.PrimaryOrgUnitID = primaryOrgUnit.ID
user.PrimaryOrgUnitName = primaryOrgUnit.Name
@@ -1088,6 +1128,37 @@ func parseWorksmobileDirectoryUser(resource map[string]any) WorksmobileRemoteUse
return user
}
func normalizeWorksmobileAccountStatus(raw string, active bool, awaiting bool, pending bool, suspended bool, deleted bool) string {
status := strings.ToLower(strings.TrimSpace(raw))
status = strings.ReplaceAll(status, "-", "_")
status = strings.ReplaceAll(status, " ", "_")
switch status {
case "deleted", "delete", "removed":
return "deleted"
case "suspended", "suspend", "blocked", "disabled":
return "suspended"
case "invited", "invite", "awaiting", "pending", "waiting", "not_activated", "unactivated":
return "invited"
case "inactive", "deactivated", "false":
return "inactive"
case "active", "enabled", "true":
return "active"
}
if deleted {
return "deleted"
}
if suspended {
return "suspended"
}
if awaiting || pending {
return "invited"
}
if !active {
return "inactive"
}
return "active"
}
func parseWorksmobileDirectoryGroup(resource map[string]any) WorksmobileRemoteGroup {
email := firstStringFromMap(resource, "email", "mail", "groupEmail", "mailingList", "orgUnitEmail", "loginId", "userName")
return WorksmobileRemoteGroup{

View File

@@ -64,6 +64,26 @@ func TestWorksmobileHTTPClientCreateUserPostsDirectoryAdminPasswordPayload(t *te
require.Len(t, passwordConfig["password"], 16)
}
func TestWorksmobileHTTPClientDeleteUserUsesDirectDirectoryDeleteForEmail(t *testing.T) {
transport := &captureRoundTripper{
statusCode: http.StatusOK,
body: `{}`,
}
client := &WorksmobileHTTPClient{
BaseURL: "https://works.example.test",
DirectoryToken: "directory-token-1",
HTTPClient: &http.Client{Transport: transport},
}
err := client.DeleteUser(context.Background(), "target@samaneng.com")
require.NoError(t, err)
require.Len(t, transport.requests, 1)
require.Equal(t, http.MethodDelete, transport.requests[0].Method)
require.Equal(t, "/v1.0/users/target@samaneng.com", transport.requests[0].URL.Path)
require.Equal(t, "Bearer directory-token-1", transport.requests[0].Header.Get("Authorization"))
}
func TestNewWorksmobileUserPatchPayloadNormalizesMalformedKoreanCellPhone(t *testing.T) {
payload := NewWorksmobileUserPatchPayload(WorksmobileUserPayload{
DomainID: 1001,
@@ -975,6 +995,27 @@ func TestCompareWorksmobileUsersIncludesBaronAndWorksPrimaryOrg(t *testing.T) {
require.Equal(t, "WORKS 기술기획", items[0].WorksmobilePrimaryOrgName)
}
func TestCompareWorksmobileUsersIncludesWorksAccountStatus(t *testing.T) {
localUsers := []domain.User{
{ID: "user-1", Email: "suspended@samaneng.com", Name: "Suspended"},
}
remoteUsers := []WorksmobileRemoteUser{
{
ID: "works-1",
ExternalID: "user-1",
Email: "suspended@samaneng.com",
DisplayName: "Suspended",
Active: false,
IsSuspended: true,
},
}
items := compareWorksmobileUsers(localUsers, remoteUsers, true, nil)
require.Len(t, items, 1)
require.Equal(t, "suspended", items[0].WorksmobileAccountStatus)
}
func TestCompareWorksmobileUsersMarksEmailMatchWithoutExternalIDNeedsUpdate(t *testing.T) {
localUsers := []domain.User{
{ID: "user-1", Email: "tester@samaneng.com", Name: "Tester"},

View File

@@ -11,6 +11,8 @@ import (
"sort"
"strings"
"time"
"github.com/google/uuid"
)
const (
@@ -106,6 +108,8 @@ type WorksmobileComparisonItem struct {
BaronSlug string `json:"baronSlug,omitempty"`
BaronName string `json:"baronName,omitempty"`
BaronEmail string `json:"baronEmail,omitempty"`
BaronPhone string `json:"baronPhone,omitempty"`
BaronEmployeeNumber string `json:"baronEmployeeNumber,omitempty"`
BaronPrimaryOrgID string `json:"baronPrimaryOrgId,omitempty"`
BaronPrimaryOrgSlug string `json:"baronPrimaryOrgSlug,omitempty"`
BaronPrimaryOrgName string `json:"baronPrimaryOrgName,omitempty"`
@@ -116,6 +120,9 @@ type WorksmobileComparisonItem struct {
ExternalKey string `json:"externalKey,omitempty"`
WorksmobileName string `json:"worksmobileName,omitempty"`
WorksmobileEmail string `json:"worksmobileEmail,omitempty"`
WorksmobilePhone string `json:"worksmobilePhone,omitempty"`
WorksmobileEmployeeNumber string `json:"worksmobileEmployeeNumber,omitempty"`
WorksmobileAccountStatus string `json:"worksmobileAccountStatus,omitempty"`
WorksmobileLevelID string `json:"worksmobileLevelId,omitempty"`
WorksmobileLevelName string `json:"worksmobileLevelName,omitempty"`
WorksmobileTask string `json:"worksmobileTask,omitempty"`
@@ -571,7 +578,7 @@ func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID,
ResourceType: domain.WorksmobileResourceUser,
ResourceID: user.ID,
Action: action,
DedupeKey: "user:" + strings.ToLower(action) + ":" + user.ID,
DedupeKey: worksmobileUserSyncDedupeKey(action, user.ID),
Payload: worksmobileUserOutboxPayload(root.ID, payload, user.Status),
}
item.Payload["displayName"] = strings.TrimSpace(user.Name)
@@ -587,6 +594,10 @@ func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID,
return item, nil
}
func worksmobileUserSyncDedupeKey(action, userID string) string {
return "user:" + strings.ToLower(action) + ":" + userID + ":" + uuid.NewString()
}
func (s *worksmobileSyncService) EnqueueUserPasswordReset(ctx context.Context, tenantID, userID, credentialBatchID string) (*domain.WorksmobileOutbox, error) {
root, err := s.hanmacRoot(ctx, tenantID)
if err != nil {
@@ -880,7 +891,7 @@ func (s *worksmobileSyncService) EnqueueUserUpsertIfInScope(ctx context.Context,
ResourceType: domain.WorksmobileResourceUser,
ResourceID: user.ID,
Action: action,
DedupeKey: "user:" + strings.ToLower(action) + ":" + user.ID,
DedupeKey: worksmobileUserSyncDedupeKey(action, user.ID),
Payload: worksmobileUserOutboxPayload(root.ID, payload, user.Status),
})
}
@@ -1461,6 +1472,8 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
BaronID: user.ID,
BaronName: user.Name,
BaronEmail: user.Email,
BaronPhone: user.Phone,
BaronEmployeeNumber: metadataEmployeeNumber(user.Metadata),
BaronPrimaryOrgID: worksmobileUserPrimaryOrgID(user),
BaronPrimaryOrgSlug: worksmobileUserPrimaryOrgSlug(user, localTenants),
BaronPrimaryOrgName: worksmobileUserPrimaryOrgName(user, localTenants),
@@ -1483,6 +1496,9 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
item.ExternalKey = remote.ExternalID
item.WorksmobileName = remote.DisplayName
item.WorksmobileEmail = remote.Email
item.WorksmobilePhone = remote.CellPhone
item.WorksmobileEmployeeNumber = remote.EmployeeNumber
item.WorksmobileAccountStatus = worksmobileRemoteAccountStatus(remote)
item.WorksmobileLevelID = remote.LevelID
item.WorksmobileLevelName = remote.LevelName
item.WorksmobileTask = remote.Task
@@ -1511,6 +1527,9 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
ExternalKey: remote.ExternalID,
WorksmobileName: remote.DisplayName,
WorksmobileEmail: remote.Email,
WorksmobilePhone: remote.CellPhone,
WorksmobileEmployeeNumber: remote.EmployeeNumber,
WorksmobileAccountStatus: worksmobileRemoteAccountStatus(remote),
WorksmobileLevelID: remote.LevelID,
WorksmobileLevelName: remote.LevelName,
WorksmobileTask: remote.Task,
@@ -1532,6 +1551,9 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
ExternalKey: remote.ExternalID,
WorksmobileName: remote.DisplayName,
WorksmobileEmail: remote.Email,
WorksmobilePhone: remote.CellPhone,
WorksmobileEmployeeNumber: remote.EmployeeNumber,
WorksmobileAccountStatus: worksmobileRemoteAccountStatus(remote),
WorksmobileLevelID: remote.LevelID,
WorksmobileLevelName: remote.LevelName,
WorksmobileTask: remote.Task,
@@ -1549,6 +1571,17 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
return result
}
func worksmobileRemoteAccountStatus(remote WorksmobileRemoteUser) string {
return normalizeWorksmobileAccountStatus(
remote.AccountStatus,
remote.Active,
remote.IsAwaiting,
remote.IsPending,
remote.IsSuspended,
remote.IsDeleted,
)
}
func worksmobileUserNeedsUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant) bool {
if strings.TrimSpace(remote.ExternalID) != strings.TrimSpace(user.ID) {
return true

View File

@@ -204,6 +204,88 @@ func TestWorksmobileSyncServiceDoesNotAutoGenerateInitialPassword(t *testing.T)
require.Empty(t, request.PasswordConfig.Password)
}
func TestWorksmobileSyncServiceCreatesDistinctUserSyncHistoryJobs(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
tenantID := "saman-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
tenant := domain.Tenant{
ID: tenantID,
Slug: "saman",
Name: "Saman",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
target := domain.User{
ID: "target-user",
Email: "target@samaneng.com",
Name: "Target",
Status: domain.UserStatusActive,
TenantID: &tenantID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, tenantID: tenant}, list: []domain.Tenant{root, tenant}},
&fakeWorksmobileUserRepo{byID: map[string]domain.User{target.ID: target}, byTenant: []domain.User{target}},
outboxRepo,
nil,
)
first, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "", "")
require.NoError(t, err)
second, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "", "")
require.NoError(t, err)
require.NotNil(t, first)
require.NotNil(t, second)
require.Len(t, outboxRepo.created, 2)
require.NotEqual(t, outboxRepo.created[0].DedupeKey, outboxRepo.created[1].DedupeKey)
}
func TestWorksmobileSyncServiceCreatesDistinctAutomaticUserSyncHistoryJobs(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
tenantID := "saman-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
tenant := domain.Tenant{
ID: tenantID,
Slug: "saman",
Name: "Saman",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
target := domain.User{
ID: "target-user",
Email: "target@samaneng.com",
Name: "Target",
Status: domain.UserStatusActive,
TenantID: &tenantID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, tenantID: tenant}, list: []domain.Tenant{root, tenant}},
&fakeWorksmobileUserRepo{byID: map[string]domain.User{target.ID: target}, byTenant: []domain.User{target}},
outboxRepo,
nil,
)
require.NoError(t, service.EnqueueUserUpsertIfInScope(context.Background(), target))
require.NoError(t, service.EnqueueUserUpsertIfInScope(context.Background(), target))
require.Len(t, outboxRepo.created, 2)
require.NotEqual(t, outboxRepo.created[0].DedupeKey, outboxRepo.created[1].DedupeKey)
}
func TestWorksmobileSyncServiceEnqueuesUserPasswordResetCredentialBatch(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
@@ -2050,6 +2132,10 @@ func TestCompareWorksmobileUsersMarksPhoneAndEmployeeNumberChangesNeedsUpdate(t
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
require.Equal(t, user.Phone, items[0].BaronPhone)
require.Equal(t, "+821099998888", items[0].WorksmobilePhone)
require.Equal(t, "EMP001", items[0].BaronEmployeeNumber)
require.Equal(t, "EMP999", items[0].WorksmobileEmployeeNumber)
}
func TestCompareWorksmobileUsersMarksMalformedRemoteKoreanPhoneNeedsUpdate(t *testing.T) {

View File

@@ -236,21 +236,12 @@ services:
# 기본 RP (Admin Front 등) 자동 등록 컨테이너
init-rp:
image: alpine:latest
image: oryd/hydra:${HYDRA_CLI_VERSION:-v26.2.0}
env_file:
- .env
entrypoint: ["/bin/sh", "-ec"]
command:
- /bin/sh
- -ec
- |
apk add --no-cache curl tar
HYDRA_CLI_VERSION="$${HYDRA_VERSION:-v26.2.0}"
HYDRA_CLI_VERSION="$${HYDRA_CLI_VERSION%-distroless}"
HYDRA_CLI_ARCHIVE_VERSION="$${HYDRA_CLI_VERSION#v}"
curl -fsSLo /tmp/hydra.tar.gz "https://github.com/ory/hydra/releases/download/$${HYDRA_CLI_VERSION}/hydra_$${HYDRA_CLI_ARCHIVE_VERSION}-linux_64bit.tar.gz"
tar -xzf /tmp/hydra.tar.gz -C /usr/local/bin hydra
rm /tmp/hydra.tar.gz
hydra delete oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" adminfront >/dev/null 2>&1 || true
hydra delete oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" devfront >/dev/null 2>&1 || true
hydra delete oauth2-client --endpoint "$${HYDRA_ADMIN_URL}" orgfront >/dev/null 2>&1 || true

View File

@@ -55,6 +55,10 @@ services:
build:
context: .
dockerfile: ./adminfront/Dockerfile
args:
VITE_ADMIN_PUBLIC_URL: ${ADMINFRONT_URL}
VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY}
VITE_OIDC_CLIENT_ID: adminfront
container_name: baron_adminfront
env_file:
- .env
@@ -80,6 +84,10 @@ services:
build:
context: .
dockerfile: ./devfront/Dockerfile
args:
VITE_DEVFRONT_PUBLIC_URL: ${DEVFRONT_URL}
VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY}
VITE_OIDC_CLIENT_ID: devfront
container_name: baron_devfront
env_file:
- .env
@@ -105,6 +113,10 @@ services:
build:
context: .
dockerfile: ./orgfront/Dockerfile
args:
VITE_ORGFRONT_PUBLIC_URL: ${ORGFRONT_URL}
VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY}
VITE_OIDC_CLIENT_ID: orgfront
container_name: baron_orgfront
env_file:
- .env
@@ -172,6 +184,33 @@ services:
networks:
- baron_net
promtail:
image: grafana/promtail:2.9.0
container_name: baron_promtail
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./docker/promtail-config.template.yaml:/etc/promtail/promtail-config.yaml:ro
command: -config.file=/etc/promtail/promtail-config.yaml -config.expand-env=true
environment:
- LOKI_URL=${LOKI_URL:-http://loki:3100/loki/api/v1/push}
- APP_ENV=${APP_ENV:-development}
networks:
- baron_net
blackbox-exporter:
image: prom/blackbox-exporter:v0.25.0
container_name: baron_blackbox_exporter
restart: unless-stopped
ports:
- "9115:9115"
volumes:
- ./docker/monitor/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
networks:
- baron_net
- ory-net
networks:
baron_net:
external: true

View File

@@ -1,6 +1,8 @@
APP_ENV=dev
APP_ENV=stage
BACKEND_LOG_LEVEL=debug
CLIENT_LOG_DEBUG=true
WORKS_ADMIN_API_BASE_URL=https://www.worksapis.com
WORKS_ADMIN_OAUTH_TOKEN_URL=REDACTED
TZ=Asia/Seoul
IDP_PROVIDER=ory
@@ -16,6 +18,7 @@ CLICKHOUSE_PASSWORD=REDACTED
BACKEND_PORT=3000
ADMINFRONT_PORT=5173
DEVFRONT_PORT=5174
ORGFRONT_PORT=
USERFRONT_PORT=5000
OATHKEEPER_API_URL=http://oathkeeper:4456
@@ -26,10 +29,11 @@ DB_NAME=baron_sso
COOKIE_SECRET=REDACTED
JWT_SECRET=REDACTED
REDIS_ADDR=redis:6389
CORS_ALLOWED_ORIGINS='*'
CORS_ALLOWED_ORIGINS=https://sso.hmac.kr
AUDIT_WORKER_COUNT=5
AUDIT_QUEUE_SIZE=2000
PROFILE_CACHE_TTL=
PROFILE_CACHE_TTL=30m
ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=3600
NAVER_CLOUD_ACCESS_KEY=REDACTED
NAVER_CLOUD_SECRET_KEY=REDACTED
NAVER_CLOUD_SERVICE_ID=ncp:sms:kr:364022321777:baroncs
@@ -38,19 +42,15 @@ AWS_REGION=ap-northeast-2
AWS_ACCESS_KEY_ID=REDACTED
AWS_SECRET_ACCESS_KEY=REDACTED
AWS_SES_SENDER=support@baroncs.co.kr
# ADMIN_EMAIL=admin@hmac.kr
ADMIN_EMAIL=su-@samaneng.com
ADMIN_EMAIL=admin@hmac.kr
ADMIN_PASSWORD=REDACTED
USERFRONT_URL=http://localhost:5000
# USERFRONT_URL=http://172.16.9.189:5000
ADMINFRONT_URL=http://localhost:5173
DEVFRONT_URL=http://localhost:5174
VITE_ORGCHART_URL=http://localhost:5175
ORGFRONT_URL=http://localhost:5175
USERFRONT_URL=https://sso.hmac.kr
ADMINFRONT_URL=https://sadmin.hmac.kr
DEVFRONT_URL=https://sdev.hmac.kr
ORGFRONT_URL=https://sorg.hmac.kr
BACKEND_PUBLIC_URL=${USERFRONT_URL}
BACKEND_URL=${USERFRONT_URL}
# OATHKEEPER_PUBLIC_URL=http://172.16.9.189:5000
OATHKEEPER_PUBLIC_URL=http://localhost:5000
OATHKEEPER_PUBLIC_URL=https://sso.hmac.kr
ORY_POSTGRES_TAG=17-trixie
ORY_POSTGRES_USER=ory
@@ -60,15 +60,16 @@ KRATOS_DB=ory_kratos
HYDRA_DB=ory_hydra
KETO_DB=ory_keto
KRATOS_VERSION=v26.2.0-distroless
KRATOS_UI_NODE_VERSION=v26.2.0
HYDRA_VERSION=v26.2.0-distroless
KETO_VERSION=v26.2.0-distroless
ORY_SDK_URL=http://kratos:4433
KRATOS_PUBLIC_URL=http://kratos:4433
KRATOS_ADMIN_URL=http://kratos:4434
KRATOS_BROWSER_URL=http://localhost:5000/auth
KRATOS_UI_URL=http://localhost:5000
KRATOS_BROWSER_URL=https://sso.hmac.kr/auth
KRATOS_UI_URL=https://sso.hmac.kr
HYDRA_ADMIN_URL=http://hydra:4445
HYDRA_PUBLIC_URL=http://localhost:5000/oidc
HYDRA_PUBLIC_URL=https://sso.hmac.kr/oidc
JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json
OATHKEEPER_VERSION=v26.2.0
OATHKEEPER_UID=1001
@@ -80,40 +81,17 @@ OATHKEEPER_HEALTH_ENABLED=true
CSRF_COOKIE_NAME=REDACTED
CSRF_COOKIE_SECRET=REDACTED
# Frontend OIDC configs for Staging
VITE_OIDC_AUTHORITY=http://localhost:5000/oidc
ADMINFRONT_CALLBACK_URLS=http://localhost:5173/auth/callback
DEVFRONT_CALLBACK_URLS=http://localhost:5174/auth/callback
ORGFRONT_CALLBACK_URLS=http://localhost:5175/auth/callback
# Frontend/Ory URL configs for Staging
VITE_OIDC_AUTHORITY=https://sso.hmac.kr/oidc
ADMINFRONT_CALLBACK_URLS=https://sadmin.hmac.kr/auth/callback
DEVFRONT_CALLBACK_URLS=https://sdev.hmac.kr/auth/callback
ORGFRONT_CALLBACK_URLS=https://sorg.hmac.kr/auth/callback
KRATOS_ALLOWED_RETURN_URLS_JSON=
KRATOS_ALLOWED_RETURN_URLS_EXTRA=
# OATHKEEPER_INTROSPECT_CLIENT_ID=
# OATHKEEPER_INTROSPECT_CLIENT_SECRET=
#Worksmobile
SAMAN_DOMAIN_ID=300285955
HANMAC_DOMAIN_ID=300286336
GPDTDC_DOMAIN_ID=300286337
BARONGROUP_DOMAIN_ID=300286645
HALLA_DOMAIN_ID=300293726
SAMAN_TENANT_ID=300285955
SAMAN_SCIM_LONGLIVE_TOKEN=REDACTED
WORKS_ADMIN_OAUTH_CLIENT_ID=JrD1iPz73ugTFV5XL_zO
WORKS_ADMIN_OAUTH_CLIENT_SECRET=REDACTED
WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT=e3n9j.serviceaccount@samaneng.com
WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE=REDACTED
WORKS_DEFAULT_DOMAIN_SAMAN=samaneng.com
WORKS_DEFAULT_DOMAIN_HANMAC=hanmaceng.co.kr
WORKS_DEFAULT_DOMAIN_GPDTDC=baroncs.co.kr
WORKS_DEFAULT_DOMAIN_BARONGROUP=brsw.kr
WORKS_DEFAULT_DOMAIN_HALLA=hallasanup.com
WORKS_ADMIN_API_BASE_URL=https://www.worksapis.com
WORKS_ADMIN_OAUTH_TOKEN_URL=REDACTED
WORKS_DRIVE_OAUTH_CLIENT_ID=9JapAnmjI9M_1SqDp4Uj
WORKS_DRIVE_OAUTH_CLIENT_SECRET=REDACTED
WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT=h4bq6.serviceaccount@samaneng.com
WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY_FILE=REDACTED
WORKS_DRIVE_APP_PASSWORD=REDACTED
WORKS_DRIVE_OAUTH_REDIRECT_URI=https://drive.hmac.kr/works/callback
WORKS_DRIVE_OAUTH_REFRESH_TOKEN=REDACTED
WORKS_DRIVE_SHARED_DRIVE_ID=@2001000000540386
WORKS_DRIVE_PARENT_FILE_ID=QDIwMDEwMDAwMDA1NDAzODZ8MzQ3MjYxMzYwMzE0NjY2NDk2OXxEfDA
# Monitoring & Alerts
SMS_WEBHOOK_PORT=8080
MONITOR_RECIPIENT_PHONES=01012345678,01098765432
LOKI_URL=http://llm_gateway_loki:3100/loki/api/v1/push

Binary file not shown.

View File

@@ -16,6 +16,7 @@ RUN apt-get update \
postgresql-client \
sed \
tar \
unzip \
util-linux \
zstd \
&& rm -rf /var/lib/apt/lists/*

View File

@@ -2,4 +2,4 @@
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKUP="${BACKUP:-${1:-}}" DUMP_FILE="${DUMP_FILE:-}" RESTORE_REPORT="${RESTORE_REPORT:-}" "$script_dir/restore.sh" --dry-run
RESTORE_INPUT="${RESTORE_INPUT:-${FILE_PATH:-${1:-}}}" BACKUP="${BACKUP:-}" DUMP_FILE="${DUMP_FILE:-}" RESTORE_REPORT="${RESTORE_REPORT:-}" "$script_dir/restore.sh" --dry-run

View File

@@ -14,6 +14,7 @@ if [[ "${1:-}" == "--dry-run" ]]; then
fi
repo_root="$(backup_repo_root)"
restore_input="${RESTORE_INPUT:-${FILE_PATH:-}}"
backup_input="${BACKUP:-}"
dump_file="${DUMP_FILE:-}"
backup_source="directory"
@@ -107,8 +108,36 @@ trap on_restore_error ERR
trap cleanup_restore_input EXIT
resolve_backup_input() {
if [[ -n "$backup_input" && -n "$dump_file" ]]; then
backup_die "set only one of BACKUP or DUMP_FILE for restore."
local input_count=0
[[ -n "$restore_input" ]] && input_count=$((input_count + 1))
[[ -n "$backup_input" ]] && input_count=$((input_count + 1))
[[ -n "$dump_file" ]] && input_count=$((input_count + 1))
if [[ "$input_count" -gt 1 ]]; then
backup_die "set only one restore input: RESTORE_INPUT, BACKUP, or DUMP_FILE."
fi
if [[ -n "$restore_input" ]]; then
backup_require_path "$restore_input"
if [[ -d "$restore_input" ]]; then
backup_dir="$restore_input"
backup_source="directory"
return
fi
if [[ ! -f "$restore_input" ]]; then
backup_die "restore input must be a backup directory or supported archive: $restore_input"
fi
case "$restore_input" in
*.tar.zst | *.tar.gz | *.tgz | *.zip)
dump_file="$restore_input"
;;
*)
backup_die "unsupported restore input file extension: $restore_input"
;;
esac
fi
if [[ -n "$backup_input" ]]; then
@@ -134,6 +163,10 @@ resolve_backup_input() {
*.tar.gz | *.tgz)
tar -xzf "$dump_file" -C "$temp_extract_dir"
;;
*.zip)
backup_require_command unzip
unzip -q "$dump_file" -d "$temp_extract_dir"
;;
*)
backup_die "unsupported DUMP_FILE archive format: $dump_file"
;;

View File

@@ -2,6 +2,9 @@
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
positional_restore_path="/tmp/baron-sso-restore-positional.tar.gz"
trap 'rm -f "$positional_restore_path"' EXIT INT TERM
: >"$positional_restore_path"
fail() {
echo "ERROR: $*" >&2
@@ -22,6 +25,8 @@ grep -Fq "docker-cli" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must include docker CLI for containerized dump/restore orchestration."
grep -Fq "perl" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must include perl for legacy ClickHouse schema dump decoding."
grep -Fq "unzip" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must include unzip for .zip restore archives."
dump_dry_run="$(
make --dry-run --always-make -C "$repo_root" dump DUMP_SERVICES="postgres,config" DUMP_MODE="maintenance" 2>&1
@@ -48,6 +53,24 @@ assert_dry_run_contains "$restore_dry_run" "DUMP_FILE=\"backups/example.tar.zst\
assert_dry_run_contains "$restore_dry_run" "RESTORE_SERVICES=\"postgres,config\""
assert_dry_run_contains "$restore_dry_run" "CONFIRM_RESTORE=\"baron-sso\""
assert_dry_run_contains "$restore_dry_run" "RESTORE_REPORT=\"reports/restore-report.json\""
assert_dry_run_contains "$restore_dry_run" "Ensuring restore target containers"
assert_dry_run_contains "$restore_dry_run" "ensure_restore_container baron_postgres compose.infra.yaml postgres"
assert_dry_run_contains "$restore_dry_run" "ensure_restore_container ory_postgres compose.ory.yaml postgres"
restore_file_path_dry_run="$(
make --dry-run --always-make -C "$repo_root" restore FILE_PATH="backups/example.tar.zst" RESTORE_SERVICES="postgres" CONFIRM_RESTORE="baron-sso" 2>&1
)"
assert_dry_run_contains "$restore_file_path_dry_run" "RESTORE_INPUT=\"backups/example.tar.zst\""
assert_dry_run_contains "$restore_file_path_dry_run" "RESTORE_SERVICES=\"postgres\""
assert_dry_run_contains "$restore_file_path_dry_run" "CONFIRM_RESTORE=\"baron-sso\""
restore_positional_dry_run="$(
make --dry-run --always-make -C "$repo_root" restore "$positional_restore_path" RESTORE_SERVICES="config" CONFIRM_RESTORE="baron-sso" 2>&1
)"
assert_dry_run_contains "$restore_positional_dry_run" "RESTORE_INPUT=\"$positional_restore_path\""
assert_dry_run_contains "$restore_positional_dry_run" "RESTORE_SERVICES=\"config\""
for target in dump-verify restore-verify dump-list restore-plan; do
target_dry_run="$(

52
test/make_help_target_test.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "ERROR: $*" >&2
exit 1
}
assert_contains() {
local output="$1"
local expected="$2"
grep -Fq -- "$expected" <<<"$output" || fail "help output must contain: $expected"
}
help_output="$(
make -C "$repo_root" help 2>&1
)"
assert_contains "$help_output" "Usage:"
assert_contains "$help_output" "Targets:"
assert_contains "$help_output" "Options:"
assert_contains "$help_output" "Restore Safety:"
for target in up dev code-check dump restore-plan code-check-userfront-e2e-tests; do
assert_contains "$help_output" "$target"
done
for option in DEV_SERVICES CODE_CHECK_TEST_JOBS PLAYWRIGHT_WORKERS BACKUP_USE_DOCKER DUMP_SERVICES RESTORE_SERVICES; do
assert_contains "$help_output" "$option"
done
for restore_usage in \
"CONFIRM_RESTORE=baron-sso" \
"ALLOW_NON_EMPTY_RESTORE=true" \
"make restore-plan FILE_PATH=stg.today.tar.gz CONFIRM_RESTORE=baron-sso" \
"make restore FILE_PATH=stg.today.tar.gz CONFIRM_RESTORE=baron-sso ALLOW_NON_EMPTY_RESTORE=true"; do
assert_contains "$help_output" "$restore_usage"
done
for description in \
"전체 로컬 스택 실행" \
"개발 앱 컨테이너를 포그라운드로 실행" \
"로컬 CI 상당 코드 검사 실행" \
"백업 덤프 생성" \
"복구 실행 계획 출력" \
"UserFront WASM E2E 테스트 실행"; do
assert_contains "$help_output" "$description"
done
echo "OK: make help exposes generated targets and configurable options"

76
test/restore_input_path_test.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d /tmp/baron-sso-restore-input.XXXXXX)"
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT INT TERM
fail() {
echo "ERROR: $*" >&2
exit 1
}
assert_contains() {
local output="$1"
local expected="$2"
grep -Fq -- "$expected" <<<"$output" || fail "output must contain: $expected"
}
run_restore_plan() {
local output
output="$("$@" 2>&1)"
assert_contains "$output" "Restore plan for"
assert_contains "$output" "Services: config"
}
backup_dir="$tmp_dir/baron-sso-backup-test"
mkdir -p "$backup_dir"
printf '{"restore_policy":{}}\n' >"$backup_dir/manifest.json"
tar_zst="$tmp_dir/baron-sso-backup-test.tar.zst"
tar_gz="$tmp_dir/baron-sso-backup-test.tar.gz"
zip_file="$tmp_dir/baron-sso-backup-test.zip"
unsupported_file="$tmp_dir/baron-sso-backup-test.tar.bz2"
tar --zstd -cf "$tar_zst" -C "$tmp_dir" "$(basename "$backup_dir")"
tar -czf "$tar_gz" -C "$tmp_dir" "$(basename "$backup_dir")"
(cd "$tmp_dir" && python3 -m zipfile -c "$zip_file" "$(basename "$backup_dir")")
: >"$unsupported_file"
run_restore_plan \
env RESTORE_INPUT="$backup_dir" RESTORE_SERVICES=config CONFIRM_RESTORE=baron-sso RESTORE_REPORT="$tmp_dir/dir-report.json" \
"$repo_root/scripts/backup/restore.sh" --dry-run
run_restore_plan \
env RESTORE_INPUT="$tar_zst" RESTORE_SERVICES=config CONFIRM_RESTORE=baron-sso RESTORE_REPORT="$tmp_dir/tar-zst-report.json" \
"$repo_root/scripts/backup/restore.sh" --dry-run
run_restore_plan \
env RESTORE_INPUT="$tar_gz" RESTORE_SERVICES=config CONFIRM_RESTORE=baron-sso RESTORE_REPORT="$tmp_dir/tar-gz-report.json" \
"$repo_root/scripts/backup/restore.sh" --dry-run
run_restore_plan \
env RESTORE_INPUT="$zip_file" RESTORE_SERVICES=config CONFIRM_RESTORE=baron-sso RESTORE_REPORT="$tmp_dir/zip-report.json" \
"$repo_root/scripts/backup/restore.sh" --dry-run
run_restore_plan \
env DUMP_FILE="$zip_file" RESTORE_SERVICES=config CONFIRM_RESTORE=baron-sso RESTORE_REPORT="$tmp_dir/direct-zip-report.json" \
"$repo_root/scripts/backup/restore.sh" --dry-run
if env RESTORE_INPUT="$unsupported_file" RESTORE_SERVICES=config CONFIRM_RESTORE=baron-sso RESTORE_REPORT="$tmp_dir/unsupported-report.json" \
"$repo_root/scripts/backup/restore.sh" --dry-run >/tmp/baron-sso-restore-unsupported.out 2>&1; then
fail "unsupported restore archive extension must fail."
fi
assert_contains "$(cat /tmp/baron-sso-restore-unsupported.out)" "unsupported restore input file extension"
if env RESTORE_INPUT="$backup_dir" BACKUP="$backup_dir" RESTORE_SERVICES=config CONFIRM_RESTORE=baron-sso RESTORE_REPORT="$tmp_dir/ambiguous-report.json" \
"$repo_root/scripts/backup/restore.sh" --dry-run >/tmp/baron-sso-restore-ambiguous.out 2>&1; then
fail "ambiguous restore inputs must fail."
fi
assert_contains "$(cat /tmp/baron-sso-restore-ambiguous.out)" "set only one restore input"
echo "OK: restore input path inference supports directories, tar archives, and zip archives"

View File

@@ -0,0 +1,94 @@
email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,sub_email,비고
sgkim6@jangheon.com,김승국,010-4847-5596,user,jangheon,,부사장,대표이사,,M02302,civilksk@jangheon.com,
mjkim7@jangheon.com,김미자,010-5502-0787,user,jangheon-business-support,,과장,,,J10313,kmj@jangheon.com,
mkchae@jangheon.com,최만규,010-4056-7247,user,jangheon-business-support,,과장,,,J22305,mkchae2@jangheon.com,
smlee3@jangheon.com,이세민,010-4764-0606,user,jangheon-production,,부장,,,P13302,jini3474@jangheon.com,
dcchoi@jangheon.com,최동찬,010-9554-1212,user,jangheon-production,,과장,,,J22301,cdc0601@jangheon.com,
ibkim@jangheon.com,김인범,010-8033-9739,user,jangheon-production,,사원,,,J23308,dlsqja9739@jangheon.com,
syhan@brsw.kr,한상연,010-8600-2267,user,baroncs,,,,,B24029,ko85han@naver.com,삼안 생성 이니셜 동일하게 설정
hkkang@brsw.kr,강호광,010-2041-5974,user,baroncs,,,,,B26008,,삼안 생성 이니셜 동일하게 설정
evenlee@brsw.kr,이용운,010-3761-9642,user,baroncs,,,,,J06102,ywlee@hanmaceng.co.kr,삼안 생성 이니셜 동일하게 설정
atom20002@brsw.kr,조영훈,010-2925-4600,user,baroncs,,,,,T02230,,삼안 생성 이니셜 동일하게 설정
gsyang@brsw.kr,양규순,010-4220-8962,user,baroncs,,,,,B22006,,삼안 생성 이니셜 동일하게 설정
dhshin1@brsw.kr,신동호,010-7247-1113,user,baroncs,,,,,B22012,realtajoal@naver.com,삼안 생성 이니셜 동일하게 설정
shmoon1@brsw.kr,문수혁,010-4165-0386,user,baroncs,,,,,B21354,b21354@hanmaceng.co.kr,삼안 생성 이니셜 동일하게 설정
yjlee1@brsw.kr,이영진,010-2080-6816,user,baroncs,,,,,B20338,b20338@hanmaceng.co.kr,삼안 생성 이니셜 동일하게 설정
dkkwon@brsw.kr,권대경,010-8031-4206,user,baroncs,,,,,B20323,dkk0425@naver.com,삼안 생성 이니셜 동일하게 설정
yskim2@brsw.kr,김예슬,010-7258-5687,user,baroncs,,,,,B20337,b20337@hanmaceng.co.kr,삼안 생성 이니셜 동일하게 설정
cjbang@brsw.kr,방찬종,010-8536-3668,user,baroncs,,,,,B22021,b22021@hanmaceng.co.kr,삼안 생성 이니셜 동일하게 설정
jwchoi6@brsw.kr,최재원,010-8562-4709,user,baroncs,,,,,B26005,,삼안 생성 이니셜 동일하게 설정
jmhyen@brsw.kr,전미현,010-6369-7053,user,baroncs,,,,,B20328,jmh@hallasanup.com,
smyoo2@brsw.kr,유승민A,010-9244-6437,user,baroncs,,,,,B23048,smin@hallasanup.com,
chjung@pre-cast.co.kr,정충화,010-4146-2835,,ptc-project-management,사업관리팀,이사,팀장,,P06208,jung@pre-cast.co.kr,
josb@pre-cast.co.kr,조성백,010-2724-8892,,ptc-project-management,사업관리팀,차장,,,P20201,josb@pre-cast.co.kr,
nksung@pre-cast.co.kr,남궁성,010-9595-8283,,ptc-project-management,사업관리팀,과장,,,P24303,sunga13@hanmaceng.co.kr,
skkim1@pre-cast.co.kr,김성규,010-9441-3823,,ptc-construction,시공팀,부장,,,P09306,winner1293@pre-cast.co.kr,
hslee6@pre-cast.co.kr,이효상,010-3879-5938,,ptc-design,설계팀,부장,,,P10301,madbaby17@pre-cast.co.kr,
kimks@pre-cast.co.kr,김경수,010-9466-1653,,ptc-construction,시공팀,부장,,,P11302,diejsa@pre-cast.co.kr,
swkim5@pre-cast.co.kr,김상욱,010-4857-3636,,tdc,기술개발센터,수석,팀장,,P11202,P11202@pre-cast.co.kr,
eschoi1@pre-cast.co.kr,최은석,010-3218-7208,,ptc-sales,영업팀,상무,팀장,,P11201,viewso@pre-cast.co.kr,
yskim10@pre-cast.co.kr,김용선,010-4026-5196,,ptc-design,설계팀,부장,,,P16203,sunofseoul@pre-cast.co.kr,
sjyoon@pre-cast.co.kr,윤성재,010-8942-3749,,ptc-sales,영업팀,부장,,,P17201,yoonsj0328@pre-cast.co.kr,
jhchoi11@pre-cast.co.kr,최종석,010-4488-3672,,ptc-construction,시공팀,차장,,,P17301,nada3672@pre-cast.co.kr,
shkim15@pre-cast.co.kr,김해성,010-4660-4410,,ptc-construction,시공팀,전무,팀장,,P17202,haiskk@pre-cast.co.kr,
jykim5@pre-cast.co.kr,김지영,010-7412-1729,,tdc,기술개발센터,책임,,,P20202,P20202@pre-cast.co.kr,
sekim2@pre-cast.co.kr,김송은,010-4090-6977,,ptc-design,설계팀,차장,,,P19303,ssongeun@pre-cast.co.kr,
hwkang@pre-cast.co.kr,강현욱,010-3352-3444,,ptc-sales,영업팀,부장,,,P19203,khwgogo@pre-cast.co.kr,
ysmun2@pre-cast.co.kr,문영석,010-2833-5718,,tdc,기술개발센터,선임,,,P20305,P20305@pre-cast.co.kr,
mjkim7@pre-cast.co.kr,김민준,010-3595-7611,,ptc-design,설계팀,사원,,,P22301,P22301@pre-cast.co.kr,
hrguk2@pre-cast.co.kr,국혜림,010-6477-9711,,tdc,기술개발센터,선임,,,P22304,P22304@pre-cast.co.kr,
hkryu@pre-cast.co.kr,류한규,010-9770-2445,,ptc-design,설계팀,이사,,,P22201,P22201@pre-cast.co.kr,
yssong@pre-cast.co.kr,송유석,010-2528-1174,,ptc-design,설계팀,사원,,,P22306,P22306@pre-cast.co.kr,
iyji@pre-cast.co.kr,지인용,010-2528-1174,,ptc-executive,임원,사장,,,P23301,P23301@pre-cast.co.kr,
hkchoi2@pre-cast.co.kr,최현기,010-6297-6815,,ptc-design,설계팀,부장,,,P23201,aia1324@pre-cast.co.kr,
hechoi@pre-cast.co.kr,최혜은,010-3453-2360,,tdc,기술개발센터,선임,,,P23304,P23304@pre-cast.co.kr,
shchun@hanmaceng.co.kr,전상현,010-2513-7763,,ptc-design,설계팀,전무,부서장,,P22101,san4747@hanmaceng.co.kr,
syseo@hanmaceng.co.kr,서성열,010-5408-7916,,ptc-executive,임원,부회장,,,M01103,sys55@hanmaceng.co.kr,
leeks@pre-cast.co.kr,이권수,010-2760-7826,,ptc-executive,임원,부사장,대표이사,,P15101,motion70@pre-cast.co.kr,
khlee5@pre-cast.co.kr,이경환,010-3391-3054,,,안전,사원,,,P25502,p25502@pre-cast.co.kr,
iglee@jangheon.co.kr,이인갑,010-3877-9086,,js-construction-hq,건설본부,이사,,,J25510,J25510@jangheon.co.kr,
baemg@jangheon.co.kr,배문교,010-3751-9058,,jangheon-sanup-executive,임원,사장,대표이사,,j25101,j25101@jangheon.co.kr,
jklee7@jangheon.co.kr,이종관,010-8725-7034,,jangheon-sanup-executive,임원,사장,,,M04101,M04101@jangheon.co.kr,
sygwak@jangheon.co.kr,곽승용,010-5167-5306,,js-construction-hq,건설본부,부장,,,j25602,j25602@jangheon.co.kr,
gskim@jangheon.co.kr,김갑성,010-8882-4315,,js-construction-hq,건설본부,전무,,,j24307,j24307@jangheon.co.kr,
dwkim2@jangheon.co.kr,김동원,010-4472-1337,,js-construction-hq,건설본부,과장,,,J22205,J22205@jangheon.co.kr,
rgkim@jangheon.co.kr,김량균,010-8165-1700,,jangheon-sanup-executive,임원,부사장,,,rgkim@,rgkim@jangheon.co.kr,
bskim7@jangheon.co.kr,김범석,010-9292-0111,,js-construction-hq,건설본부,부장,,,j22202,j22202@jangheon.co.kr,
sckim5@jangheon.co.kr,김상철,010-3372-3194,,js-construction-hq,건설본부,이사,,,j22202,j22202@jangheon.co.kr,
swkim5@jangheon.co.kr,김성원,010-9544-0506,,js-tech-sales-hq,기술영업본부,부장,,,yi04@j,yi04@jangheon.co.kr,
shkim7@jangheon.co.kr,김성환,010-4184-1100,,js-construction-hq,건설본부,부장,,,j25301,j25301@jangheon.co.kr,
iykim@jangheon.co.kr,김인열,010-8667-8536,,js-construction-hq,건설본부,전무,,,kiy853,kiy8536@jangheon.co.kr,
jkim@jangheon.co.kr,김진,010-4657-6970,,js-tech-sales-hq,기술영업본부,이사,,,kimjin,kimjin1971@jangheon.co.kr,
synam@jangheon.co.kr,남수용,010-3612-9067,,js-construction-hq,건설본부,이사,,,j25506,j25506@jangheon.co.kr,
jtmoon@jangheon.co.kr,문종탁,010-8752-0433,,js-tech-sales-hq,기술영업본부,부장,,,jtmoon,jtmoon@jangheon.co.kr,
swpark5@jangheon.co.kr,박선우,010-4350-3467,,js-tech-sales-hq,기술영업본부,사원,,,J24301,J24301@jangheon.co.kr,
sipark2@jangheon.co.kr,박성일,010-6266-5913,,js-construction-hq,건설본부,상무,,,j24501,j24501@jangheon.co.kr,
ebpark@jangheon.co.kr,박은별,010-8247-5542,,,안전팀,사원,,,j25305,j25305@jangheon.co.kr,
bjseo@jangheon.co.kr,서범진,010-2491-8391,,js-construction-hq,건설본부,과장,,,j25604,j25604@jangheon.co.kr,
jgan@jangheon.co.kr,안종기,010-9770-6249,,js-construction-hq,건설본부,상무,,,j25504,j25504@jangheon.co.kr,
jsyang@jangheon.co.kr,양종식,010-6859-2624,,js-construction-hq,건설본부,상무,팀장,,cejs76,cejs76@jangheon.co.kr,
dhyun@jangheon.co.kr,윤두현,010-6243-0081,,js-construction-hq,건설본부,부장,,,doohyu,doohyuny04@jangheon.co.kr,
siyoun@jangheon.co.kr,윤선일,010-7749-1076,,js-construction-hq,건설본부,차장,,,j22201,j22201@jangheon.co.kr,
cyyun2@jangheon.co.kr,윤충열,010-4560-5469,,js-construction-hq,건설본부,부장,,,J25601,J25601@jangheon.co.kr,
sslee3@jangheon.co.kr,이수식,010-3528-7895,,js-construction-hq,건설본부,이사,,,j24503,j24503@jangheon.co.kr,
shlee25@jangheon.co.kr,이의환,010-3354-2945,,js-construction-hq,건설본부,부장,,,j13308,j13308@jangheon.co.kr,
jylee9@jangheon.co.kr,이재영,010-7265-2258,,js-construction-hq,건설본부,부장,,,young7,young7471@jangheon.co.kr,
jhlee8@jangheon.co.kr,이정훈,010-9031-7789,,js-tech-sales-hq,기술영업본부,이사,주무,,J07210,fb1t@jangheon.co.kr,
jklee9@jangheon.co.kr,이중경,010-6317-9794,,js-tech-sales-hq,기술영업본부,이사,,,first7,first7777@jangheon.co.kr,
chlee7@jangheon.co.kr,이창호,010-3807-3517,,js-construction-hq,건설본부,이사,,,j24502,j24502@jangheon.co.kr,
hblee@jangheon.co.kr,이호범,010-4678-5592,,js-construction-hq,건설본부,사원,,,J23301,J23301@jangheon.co.kr,
hjlee5@jangheon.co.kr,이훈재,010-8590-3055,,jangheon-sanup-executive,임원,부사장,,,j24101,j24101@jangheon.co.kr,
wscheung@jangheon.co.kr,정우성,010-4769-9299,,js-tech-sales-hq,기술영업본부,상무,팀장,,j23307,j23307@jangheon.co.kr,
dicho@jangheon.co.kr,조동일,010-9141-9180,,js-tech-sales-hq,기술영업본부,대리,,,j21303,j21303@jangheon.co.kr,
bgcho@jangheon.co.kr,조부건,010-6358-1326,,js-construction-hq,건설본부,부장,,,j25603,j25603@jangheon.co.kr,
sgjoo@jangheon.co.kr,주상구,010-5306-5502,,,안전팀,상무,,,twojoo,twojoo96@jangheon.co.kr,
sdcha@jangheon.co.kr,차성대,010-3610-8458,,js-construction-hq,건설본부,상무,,,csd915,csd915@jangheon.co.kr,
tschoi@jangheon.co.kr,최태순,010-7118-1126,,js-construction-hq,건설본부,상무,주무,,khw04@,khw04@jangheon.co.kr,
jmhwang1@jangheon.co.kr,황재민,010-9329-1936,,js-tech-sales-hq,기술영업본부,부장,,,jmhwan,jmhwang@jangheon.co.kr,
ystyoo@jangheon.co.kr,류영섭,010-5237-4923,,jangheon-sanup-executive,,기술고문,,,J15101,,
iskim2@jangheon.co.kr,김인수,010-5374-2674,,js-construction-hq,건설본부,상무,,,kiss04,kiss0408@jangheon.co.kr,
dhkim7@jangheon.co.kr,김동환,011-721-5401,,jangheon-sanup-executive,,상임고문,,,J16301,,
jgkim2@jangheon.co.kr,김진규,010-3532-4954,,,안전팀,이사,,,j26302,j26302@jangheon.co.kr,
sychoi@jangheon.co.kr,최성용,010-9103-4302,,js-construction-hq,건설본부,대리,,,J23303,,
jwpark1@jangheon.co.kr,박지우,010-3530-2436,,js-construction-hq,건설본부,사원,,,p25301,p25301@jangheon.co.kr,
dwpark@jangheon.co.kr,박대원,010-3956-8218,,,안전팀,이사,,,J26501,J26501@jangheon.co.kr,
1 email name phone role tenant_slug department grade position jobTitle employee_id sub_email 비고
2 sgkim6@jangheon.com 김승국 010-4847-5596 user jangheon 부사장 대표이사 M02302 civilksk@jangheon.com
3 mjkim7@jangheon.com 김미자 010-5502-0787 user jangheon-business-support 과장 J10313 kmj@jangheon.com
4 mkchae@jangheon.com 최만규 010-4056-7247 user jangheon-business-support 과장 J22305 mkchae2@jangheon.com
5 smlee3@jangheon.com 이세민 010-4764-0606 user jangheon-production 부장 P13302 jini3474@jangheon.com
6 dcchoi@jangheon.com 최동찬 010-9554-1212 user jangheon-production 과장 J22301 cdc0601@jangheon.com
7 ibkim@jangheon.com 김인범 010-8033-9739 user jangheon-production 사원 J23308 dlsqja9739@jangheon.com
8 syhan@brsw.kr 한상연 010-8600-2267 user baroncs B24029 ko85han@naver.com 삼안 생성 이니셜 동일하게 설정
9 hkkang@brsw.kr 강호광 010-2041-5974 user baroncs B26008 삼안 생성 이니셜 동일하게 설정
10 evenlee@brsw.kr 이용운 010-3761-9642 user baroncs J06102 ywlee@hanmaceng.co.kr 삼안 생성 이니셜 동일하게 설정
11 atom20002@brsw.kr 조영훈 010-2925-4600 user baroncs T02230 삼안 생성 이니셜 동일하게 설정
12 gsyang@brsw.kr 양규순 010-4220-8962 user baroncs B22006 삼안 생성 이니셜 동일하게 설정
13 dhshin1@brsw.kr 신동호 010-7247-1113 user baroncs B22012 realtajoal@naver.com 삼안 생성 이니셜 동일하게 설정
14 shmoon1@brsw.kr 문수혁 010-4165-0386 user baroncs B21354 b21354@hanmaceng.co.kr 삼안 생성 이니셜 동일하게 설정
15 yjlee1@brsw.kr 이영진 010-2080-6816 user baroncs B20338 b20338@hanmaceng.co.kr 삼안 생성 이니셜 동일하게 설정
16 dkkwon@brsw.kr 권대경 010-8031-4206 user baroncs B20323 dkk0425@naver.com 삼안 생성 이니셜 동일하게 설정
17 yskim2@brsw.kr 김예슬 010-7258-5687 user baroncs B20337 b20337@hanmaceng.co.kr 삼안 생성 이니셜 동일하게 설정
18 cjbang@brsw.kr 방찬종 010-8536-3668 user baroncs B22021 b22021@hanmaceng.co.kr 삼안 생성 이니셜 동일하게 설정
19 jwchoi6@brsw.kr 최재원 010-8562-4709 user baroncs B26005 삼안 생성 이니셜 동일하게 설정
20 jmhyen@brsw.kr 전미현 010-6369-7053 user baroncs B20328 jmh@hallasanup.com
21 smyoo2@brsw.kr 유승민A 010-9244-6437 user baroncs B23048 smin@hallasanup.com
22 chjung@pre-cast.co.kr 정충화 010-4146-2835 ptc-project-management 사업관리팀 이사 팀장 P06208 jung@pre-cast.co.kr
23 josb@pre-cast.co.kr 조성백 010-2724-8892 ptc-project-management 사업관리팀 차장 P20201 josb@pre-cast.co.kr
24 nksung@pre-cast.co.kr 남궁성 010-9595-8283 ptc-project-management 사업관리팀 과장 P24303 sunga13@hanmaceng.co.kr
25 skkim1@pre-cast.co.kr 김성규 010-9441-3823 ptc-construction 시공팀 부장 P09306 winner1293@pre-cast.co.kr
26 hslee6@pre-cast.co.kr 이효상 010-3879-5938 ptc-design 설계팀 부장 P10301 madbaby17@pre-cast.co.kr
27 kimks@pre-cast.co.kr 김경수 010-9466-1653 ptc-construction 시공팀 부장 P11302 diejsa@pre-cast.co.kr
28 swkim5@pre-cast.co.kr 김상욱 010-4857-3636 tdc 기술개발센터 수석 팀장 P11202 P11202@pre-cast.co.kr
29 eschoi1@pre-cast.co.kr 최은석 010-3218-7208 ptc-sales 영업팀 상무 팀장 P11201 viewso@pre-cast.co.kr
30 yskim10@pre-cast.co.kr 김용선 010-4026-5196 ptc-design 설계팀 부장 P16203 sunofseoul@pre-cast.co.kr
31 sjyoon@pre-cast.co.kr 윤성재 010-8942-3749 ptc-sales 영업팀 부장 P17201 yoonsj0328@pre-cast.co.kr
32 jhchoi11@pre-cast.co.kr 최종석 010-4488-3672 ptc-construction 시공팀 차장 P17301 nada3672@pre-cast.co.kr
33 shkim15@pre-cast.co.kr 김해성 010-4660-4410 ptc-construction 시공팀 전무 팀장 P17202 haiskk@pre-cast.co.kr
34 jykim5@pre-cast.co.kr 김지영 010-7412-1729 tdc 기술개발센터 책임 P20202 P20202@pre-cast.co.kr
35 sekim2@pre-cast.co.kr 김송은 010-4090-6977 ptc-design 설계팀 차장 P19303 ssongeun@pre-cast.co.kr
36 hwkang@pre-cast.co.kr 강현욱 010-3352-3444 ptc-sales 영업팀 부장 P19203 khwgogo@pre-cast.co.kr
37 ysmun2@pre-cast.co.kr 문영석 010-2833-5718 tdc 기술개발센터 선임 P20305 P20305@pre-cast.co.kr
38 mjkim7@pre-cast.co.kr 김민준 010-3595-7611 ptc-design 설계팀 사원 P22301 P22301@pre-cast.co.kr
39 hrguk2@pre-cast.co.kr 국혜림 010-6477-9711 tdc 기술개발센터 선임 P22304 P22304@pre-cast.co.kr
40 hkryu@pre-cast.co.kr 류한규 010-9770-2445 ptc-design 설계팀 이사 P22201 P22201@pre-cast.co.kr
41 yssong@pre-cast.co.kr 송유석 010-2528-1174 ptc-design 설계팀 사원 P22306 P22306@pre-cast.co.kr
42 iyji@pre-cast.co.kr 지인용 010-2528-1174 ptc-executive 임원 사장 P23301 P23301@pre-cast.co.kr
43 hkchoi2@pre-cast.co.kr 최현기 010-6297-6815 ptc-design 설계팀 부장 P23201 aia1324@pre-cast.co.kr
44 hechoi@pre-cast.co.kr 최혜은 010-3453-2360 tdc 기술개발센터 선임 P23304 P23304@pre-cast.co.kr
45 shchun@hanmaceng.co.kr 전상현 010-2513-7763 ptc-design 설계팀 전무 부서장 P22101 san4747@hanmaceng.co.kr
46 syseo@hanmaceng.co.kr 서성열 010-5408-7916 ptc-executive 임원 부회장 M01103 sys55@hanmaceng.co.kr
47 leeks@pre-cast.co.kr 이권수 010-2760-7826 ptc-executive 임원 부사장 대표이사 P15101 motion70@pre-cast.co.kr
48 khlee5@pre-cast.co.kr 이경환 010-3391-3054 안전 사원 P25502 p25502@pre-cast.co.kr
49 iglee@jangheon.co.kr 이인갑 010-3877-9086 js-construction-hq 건설본부 이사 J25510 J25510@jangheon.co.kr
50 baemg@jangheon.co.kr 배문교 010-3751-9058 jangheon-sanup-executive 임원 사장 대표이사 j25101 j25101@jangheon.co.kr
51 jklee7@jangheon.co.kr 이종관 010-8725-7034 jangheon-sanup-executive 임원 사장 M04101 M04101@jangheon.co.kr
52 sygwak@jangheon.co.kr 곽승용 010-5167-5306 js-construction-hq 건설본부 부장 j25602 j25602@jangheon.co.kr
53 gskim@jangheon.co.kr 김갑성 010-8882-4315 js-construction-hq 건설본부 전무 j24307 j24307@jangheon.co.kr
54 dwkim2@jangheon.co.kr 김동원 010-4472-1337 js-construction-hq 건설본부 과장 J22205 J22205@jangheon.co.kr
55 rgkim@jangheon.co.kr 김량균 010-8165-1700 jangheon-sanup-executive 임원 부사장 rgkim@ rgkim@jangheon.co.kr
56 bskim7@jangheon.co.kr 김범석 010-9292-0111 js-construction-hq 건설본부 부장 j22202 j22202@jangheon.co.kr
57 sckim5@jangheon.co.kr 김상철 010-3372-3194 js-construction-hq 건설본부 이사 j22202 j22202@jangheon.co.kr
58 swkim5@jangheon.co.kr 김성원 010-9544-0506 js-tech-sales-hq 기술영업본부 부장 yi04@j yi04@jangheon.co.kr
59 shkim7@jangheon.co.kr 김성환 010-4184-1100 js-construction-hq 건설본부 부장 j25301 j25301@jangheon.co.kr
60 iykim@jangheon.co.kr 김인열 010-8667-8536 js-construction-hq 건설본부 전무 kiy853 kiy8536@jangheon.co.kr
61 jkim@jangheon.co.kr 김진 010-4657-6970 js-tech-sales-hq 기술영업본부 이사 kimjin kimjin1971@jangheon.co.kr
62 synam@jangheon.co.kr 남수용 010-3612-9067 js-construction-hq 건설본부 이사 j25506 j25506@jangheon.co.kr
63 jtmoon@jangheon.co.kr 문종탁 010-8752-0433 js-tech-sales-hq 기술영업본부 부장 jtmoon jtmoon@jangheon.co.kr
64 swpark5@jangheon.co.kr 박선우 010-4350-3467 js-tech-sales-hq 기술영업본부 사원 J24301 J24301@jangheon.co.kr
65 sipark2@jangheon.co.kr 박성일 010-6266-5913 js-construction-hq 건설본부 상무 j24501 j24501@jangheon.co.kr
66 ebpark@jangheon.co.kr 박은별 010-8247-5542 안전팀 사원 j25305 j25305@jangheon.co.kr
67 bjseo@jangheon.co.kr 서범진 010-2491-8391 js-construction-hq 건설본부 과장 j25604 j25604@jangheon.co.kr
68 jgan@jangheon.co.kr 안종기 010-9770-6249 js-construction-hq 건설본부 상무 j25504 j25504@jangheon.co.kr
69 jsyang@jangheon.co.kr 양종식 010-6859-2624 js-construction-hq 건설본부 상무 팀장 cejs76 cejs76@jangheon.co.kr
70 dhyun@jangheon.co.kr 윤두현 010-6243-0081 js-construction-hq 건설본부 부장 doohyu doohyuny04@jangheon.co.kr
71 siyoun@jangheon.co.kr 윤선일 010-7749-1076 js-construction-hq 건설본부 차장 j22201 j22201@jangheon.co.kr
72 cyyun2@jangheon.co.kr 윤충열 010-4560-5469 js-construction-hq 건설본부 부장 J25601 J25601@jangheon.co.kr
73 sslee3@jangheon.co.kr 이수식 010-3528-7895 js-construction-hq 건설본부 이사 j24503 j24503@jangheon.co.kr
74 shlee25@jangheon.co.kr 이의환 010-3354-2945 js-construction-hq 건설본부 부장 j13308 j13308@jangheon.co.kr
75 jylee9@jangheon.co.kr 이재영 010-7265-2258 js-construction-hq 건설본부 부장 young7 young7471@jangheon.co.kr
76 jhlee8@jangheon.co.kr 이정훈 010-9031-7789 js-tech-sales-hq 기술영업본부 이사 주무 J07210 fb1t@jangheon.co.kr
77 jklee9@jangheon.co.kr 이중경 010-6317-9794 js-tech-sales-hq 기술영업본부 이사 first7 first7777@jangheon.co.kr
78 chlee7@jangheon.co.kr 이창호 010-3807-3517 js-construction-hq 건설본부 이사 j24502 j24502@jangheon.co.kr
79 hblee@jangheon.co.kr 이호범 010-4678-5592 js-construction-hq 건설본부 사원 J23301 J23301@jangheon.co.kr
80 hjlee5@jangheon.co.kr 이훈재 010-8590-3055 jangheon-sanup-executive 임원 부사장 j24101 j24101@jangheon.co.kr
81 wscheung@jangheon.co.kr 정우성 010-4769-9299 js-tech-sales-hq 기술영업본부 상무 팀장 j23307 j23307@jangheon.co.kr
82 dicho@jangheon.co.kr 조동일 010-9141-9180 js-tech-sales-hq 기술영업본부 대리 j21303 j21303@jangheon.co.kr
83 bgcho@jangheon.co.kr 조부건 010-6358-1326 js-construction-hq 건설본부 부장 j25603 j25603@jangheon.co.kr
84 sgjoo@jangheon.co.kr 주상구 010-5306-5502 안전팀 상무 twojoo twojoo96@jangheon.co.kr
85 sdcha@jangheon.co.kr 차성대 010-3610-8458 js-construction-hq 건설본부 상무 csd915 csd915@jangheon.co.kr
86 tschoi@jangheon.co.kr 최태순 010-7118-1126 js-construction-hq 건설본부 상무 주무 khw04@ khw04@jangheon.co.kr
87 jmhwang1@jangheon.co.kr 황재민 010-9329-1936 js-tech-sales-hq 기술영업본부 부장 jmhwan jmhwang@jangheon.co.kr
88 ystyoo@jangheon.co.kr 류영섭 010-5237-4923 jangheon-sanup-executive 기술고문 J15101
89 iskim2@jangheon.co.kr 김인수 010-5374-2674 js-construction-hq 건설본부 상무 kiss04 kiss0408@jangheon.co.kr
90 dhkim7@jangheon.co.kr 김동환 011-721-5401 jangheon-sanup-executive 상임고문 J16301
91 jgkim2@jangheon.co.kr 김진규 010-3532-4954 안전팀 이사 j26302 j26302@jangheon.co.kr
92 sychoi@jangheon.co.kr 최성용 010-9103-4302 js-construction-hq 건설본부 대리 J23303
93 jwpark1@jangheon.co.kr 박지우 010-3530-2436 js-construction-hq 건설본부 사원 p25301 p25301@jangheon.co.kr
94 dwpark@jangheon.co.kr 박대원 010-3956-8218 안전팀 이사 J26501 J26501@jangheon.co.kr

119
works_users_halla.CSV Normal file
View File

@@ -0,0 +1,119 @@
email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,sub_email
khkim@hallasanup.com,김광현,010-7411-4180,,hanlla-mgmt-support-hq,,전무이사,,,HM00112,ezfree@hallasanup.com
dcsong@hallasanup.com,송동찬,010-5414-6973,,hanlla-mgmt-support,,이사,,,HM02597,sdc0327@hallasanup.com
jhlee@hallasanup.com,이준하,010-9189-9705,,hanlla-mgmt-support,,차장,,,HM01357,jhlee@hallasanup.com
wspark@hallasanup.com,박우식,010-4851-8193,,hanlla-mgmt-support,,상무이사,,,HM00417,pws9@hallasanup.com
islee@hallasanup.com,이인서,010-5405-8044,,hanlla-business-support,,부장,,,HM02581,islee@hallasanup.com
kaseo@hallasanup.com,서경아,010-5242-9794,,hanlla-business-support,,차장,,,HF00442,sky@hallasanup.com
jhlee1@hallasanup.com,이종희,010-8626-8710,,hanlla-operations,,차장,,,HM02420,jhlee01@hallasanup.com
bjyoo@hallasanup.com,유붕종,010-4519-2634,,hanlla-general-business,,전무이사,,,HM01775,yoo660327@hallasanup.com
hckim@hallasanup.com,김희철,010-3790-0273,,hanlla-operations,,부장,,,HM00564,mecha011@hallasanup.com
jhchoi@hallasanup.com,최재혁,010-8831-7961,,site-gwangju-wastewater,,부장,,,HM02713,jh7184@hallasanup.com
hjjang@hallasanup.com,장홍재,010-5024-0588,,site-gwangju-wastewater,,차장,,,HM02747,hjjang@hallasanup.com
cwpark@hallasanup.com,박찬웅,010-2519-8190,,site-gwangju-wastewater,,대리,,,HM02643,fpeld15@hallasanup.com
jwlee@hallasanup.com,이정우,010-3729-2726,,site-gwangju-wastewater,,부장,,,HM02624,ljw1069@hallasanup.com
yjkang@hallasanup.com,강영진1,010-9254-4559,,site-gwangju-wastewater,,부장,,,HM02553,bukzy@hallasanup.com
swchoi@hallasanup.com,최승우,010-3816-5053,,site-gwangju-wastewater,,부장,,,HM02735,paikiki@hallasanup.com
dkkim@hallasanup.com,김동균1,010-9125-6361,,site-gwangju-wastewater,,부장,,,HM02697,kdk73@hallasanup.com
yjlee@hallasanup.com,이여진,010-2349-8106,,site-gwangju-wastewater,,사원,,,HF02728,yeojin@hallasanup.com
bmlee@hallasanup.com,이병문,010-3565-4737,,hanlla-env-plant-hq,,전무이사,,,HM00056,bmlee@hallasanup.com
syyang@hallasanup.com,양승엽,010-6246-7872,,hanlla-infra-project-mgmt,,상무이사,,,HM01976,ysungyup@hallasanup.com
dhkim@hallasanup.com,김동현,010-2463-5874,,hanlla-infra-project-mgmt,,부장,,,HM02775,kdh0202@hallasanup.com
sjkim@hallasanup.com,김승주,010-2929-1268,,hanlla-infra-project-mgmt,,대리,,,HM02678,ksj@hallasanup.com
skyang@hallasanup.com,양승경,010-2753-4994,,hanlla-infra-project-mgmt,,과장,,,HF01029,tmdrud86@hallasanup.com
djkwon@hallasanup.com,권대준,010-2458-3373,,hanlla-general-sales,,전무이사,,,HM02570,jjoony@hallasanup.com
wgkim@hallasanup.com,김원근,010-7634-7872,,hanlla-executive,,사장,,,HM01774,kimwg7872@hallasanup.com
btkim@hallasanup.com,김범태,010-4653-5690,,site-docheok-silchon-road,,부장,,,HM02722,70kbt@hallasanup.com
skhong@hallasanup.com,홍성관,010-9458-1766,,site-docheok-silchon-road,,차장,,,HM02719,kwany@hallasanup.com
dkchoi@hallasanup.com,최덕규,010-3315-5579,,site-docheok-silchon-road,,차장,,,HM02718,cdk20001@hallasanup.com
jhkim3@hallasanup.com,김종호,010-2758-9606,,site-docheok-silchon-road,,부장,,,HM02715,civilman12@hallasanup.com
jmkoo@hallasanup.com,구자민,010-5057-3073,,site-docheok-silchon-road,,대리,,,HM02721,wkals4582@hallasanup.com
khpark@hallasanup.com,박기황,010-2527-4607,,site-docheok-silchon-road,,부장,,,HM02717,parkkhhh@hallasanup.com
ehpark@hallasanup.com,박은호,010-9740-6634,,site-docheok-silchon-road,,차장,,,HM02734,eunho6634@hallasanup.com
kjkoo@hallasanup.com,구교진,010-3151-2660,,site-docheok-silchon-road,,사원,,,HM02638,kkj0509@hallasanup.com
shhong@hallasanup.com,홍순화,010-6624-6733,,site-docheok-silchon-road,,사원,,,HF02737,hongga328@hallasanup.com
dhlee@hallasanup.com,이동훈,010-3427-8276,,site-busan-new-port,,부장,,,HM02542,dhlee88@hallasanup.com
jgsong@hallasanup.com,송재규,010-9282-8213,,site-busan-new-port,,차장,,,HM02308,jgsong@hallasanup.com
wkjung@hallasanup.com,정원근,010-8923-2936,,site-busan-new-port,,부장,,,HM00365,jwg2936@hallasanup.com
syseok@hallasanup.com,석성용2,010-5654-5175,,site-busan-new-port,,차장,,,HM02644,ssssam4080@hallasanup.com
nrha@hallasanup.com,하누리,010-3005-5453,,site-busan-new-port,,차장,,,HM01015,nurry@hallasanup.com
jsjun@hallasanup.com,전종식2,010-2285-2324,,site-busan-new-port,,부장,,,HM02736,shik2324@hallasanup.com
rklee@hallasanup.com,이령경,010-5747-0944,,site-busan-new-port,,사원,,,HF02761,kyeong951104@hallasanup.com
hslee@hallasanup.com,이한소,010-8820-4650,,site-busan-new-port,,과장,,,HM02760,lhso111@hallasanup.com
bslee@hallasanup.com,이병석,010-4520-5573,,site-busan-new-port,,부장,,,HM02766,hopesuck20@hallasanup.com
smpark@hallasanup.com,박성민3,010-3631-8923,,site-busan-new-port,,이사,,,HM02780,smpark@hallasanup.com
jichoi@hallasanup.com,최재인,010-2205-0870,,site-bucheon-gulpocheon,,차장,,,HM02511,dia044653@hallasanup.com
ytyoo@hallasanup.com,유연태,010-8735-1567,,site-bucheon-gulpocheon,,부장,,,HM02730,yyt1567@hallasanup.com
hjkim@hallasanup.com,김희준,010-6288-8751,,site-bucheon-gulpocheon,,부장,,,HM02496,s09j15e@hallasanup.com
hssong@hallasanup.com,송홍섭,010-8963-1595,,site-bucheon-gulpocheon,,차장,,,HM02714,SH1029@hallasanup.com
mgkong@hallasanup.com,공민구,010-7752-8755,,site-bucheon-gulpocheon,,대리,,,HM02603,kongkevin@hallasanup.com
jhkim2@hallasanup.com,김진혁1,010-3708-8916,,site-bucheon-gulpocheon,,사원,,,HM02711,jhk752@hallasanup.com
shshin@hallasanup.com,신상훈,010-9140-8549,,site-bucheon-gulpocheon,,부장,,,HM02748,sshun1105@hallasanup.com
ksmoon@hallasanup.com,문경수,010-5435-8013,,site-bucheon-gulpocheon,,부장,,,HM02773,mks@hallasanup.com
silee@hallasanup.com,이송이,010-8923-8029,,site-bucheon-gulpocheon,,사원,,,HF02693,lovelyanne87@hallasanup.com
hyjeong@hallasanup.com,정호영,010-8596-4936,,site-sudokwon-landfill-2,,과장,,,HM01990,jhymhn@hallasanup.com
shcho@hallasanup.com,조성훈1,010-3999-9502,,site-sudokwon-landfill-2,,부장,,,HM02534,hoon96@hallasanup.com
mkryu@hallasanup.com,류문경,010-9489-6975,,site-sudokwon-landfill-2,,부장,,,HM02751,ryu9643019@hallasanup.com
shhan@hallasanup.com,한성호,010-9393-7097,,site-sudokwon-landfill-2,,부장,,,HM02763,hsh7097@hallasanup.com
syoh@hallasanup.com,오세영,010-2868-8953,,site-sudokwon-landfill-2,,이사,,,HM01969,Osy8953530@hallasanup.com
yssong@hallasanup.com,송영석,010-2731-6730,,site-sudokwon-landfill-2,,부장,,,HM02517,sys6730@hallasanup.com
dwlee@hallasanup.com,이대원,010-2313-9963,,site-sudokwon-landfill-2,,차장,,,HM02472,leedw@hallasanup.com
rrpark@hallasanup.com,박루리,010-9146-9934,,site-sudokwon-landfill-2,,사원,,,HF02774,parkruri@hallasanup.com
pkson@hallasanup.com,손판국,010-9367-5967,,site-sincheon-sewage,,부장,,,HM02563,sagibry@hallasanup.com
mjlee@hallasanup.com,이명준,010-6399-5159,,site-sincheon-sewage,,차장,,,HM02491,myungjoon@hallasanup.com
bspark@hallasanup.com,박봉식,010-4009-2670,,site-sincheon-sewage,,과장,,,HM02733,bspark1216@hallasanup.com
mhbeak@hallasanup.com,백무현,010-2235-6749,,site-sincheon-sewage,,부장,,,HM02742,hyeonoo@hallasanup.com
jwnam@hallasanup.com,남준우,010-6305-6648,,site-sincheon-sewage,,대리,,,HM02755,nju6305200@hallasanup.com
jklee1@hallasanup.com,이중곤,010-7166-5994,,site-sincheon-sewage,,차장,,,HM02779,wndwkdia0@hallasanup.com
hjlee@hallasanup.com,이현진,010-4858-5229,,site-sincheon-sewage,,대리,,,HM02732,hjlee@hallasanup.com
suan@hallasanup.com,안소은,010-7761-5241,,site-sincheon-sewage,,사원,,,HF02746,an016363@hallasanup.com
jwjung@hallasanup.com,정재욱,010-8543-5111,,site-apo-sewage,,부장,,,HM02727,wjdwodnr77@hallasanup.com
ybkim@hallasanup.com,김유빈,010-4112-2647,,site-apo-sewage,,사원,,,HM02596,ybkim@hallasanup.com
smjoo@hallasanup.com,주성민,010-9938-8420,,site-apo-sewage,,과장,,,HM02547,jsm@hallasanup.com
jhkim@hallasanup.com,김제희,010-2703-0072,,site-apo-sewage,,부장,,,HM02551,yeskjh72@hallasanup.com
dyshin@hallasanup.com,신동엽,010-2523-3364,,site-apo-sewage,,부장,,,HM01968,shindongyop@hallasanup.com
hkjang@hallasanup.com,장현관,010-5195-0348,,site-apo-sewage,,차장,,,HM02544,eddy2001@hallasanup.com
hglee@hallasanup.com,이희곤,010-5358-1021,,site-apo-sewage,,부장,,,HM02622,lhg1021@hallasanup.com
jhkim1@hallasanup.com,김정훈1,010-5275-8622,,site-apo-sewage,,차장,,,HM02710,jhk7510@hallasanup.com
tylee@hallasanup.com,이태영,010-2805-2836,,site-apo-sewage,,부장,,,HM00813,black0112@hallasanup.com
syjeon@hallasanup.com,전소영,010-9600-2199,,site-apo-sewage,,사원,,,HF02560,jsy2220@hallasanup.com
hykwon@hallasanup.com,권해용1,010-6522-8153,,ops-anseong-wwtp,,부장,,,HM02507,khy11k@hallasanup.com
jyher@hallasanup.com,허지연,010-2735-5266,,ops-anseong-wwtp,,과장,,,HF02744,jyheo@hallasanup.com
gykim@hallasanup.com,김기영,010-5399-2520,,hanlla-safety-hq,,전무이사,,,HM00126,young@hallasanup.com
wjyun@hallasanup.com,윤원종,010-8243-8083,,hanlla-safety-team,,사원,,,HM02740,yunwj0808@hallasanup.com
ddkang@hallasanup.com,강대득,010-8738-9165,,site-yeoju-bupyeongcheon,,부장,,,HM02504,kdd3880@hallasanup.com
hypark@hallasanup.com,박흥열,010-2312-9853,,site-yeoju-bupyeongcheon,,과장,,,HM02749,as8742@hallasanup.com
kyryu@hallasanup.com,류길용,010-2621-6152,,site-yeoju-bupyeongcheon,,과장,,,HM02741,bbangrky@hallasanup.com
sklee@hallasanup.com,이상규,010-8650-0875,,site-yeoju-bupyeongcheon,,차장,,,HM02762,dorajisa@hallasanup.com
jhjung@hallasanup.com,정지호,010-9013-9596,,site-yeoju-bupyeongcheon,,부장,,,HM02765,need1971@hallasanup.com
jykim@hallasanup.com,김지용,010-5685-7038,,site-okjeong-sewage,,차장,,,HM02610,gmn1038@hallasanup.com
jschoi@hallasanup.com,최종순,010-2890-0856,,site-okjeong-sewage,,차장,,,HM02505,ddjjs@hallasanup.com
wjkim@hallasanup.com,김원준,010-3194-3222,,site-okjeong-sewage,,과장,,,HM02422,sirio@hallasanup.com
djpark@hallasanup.com,박대중,010-6561-5255,,site-okjeong-sewage,,부장,,,HM02689,ptgsun1@hallasanup.com
wslee@hallasanup.com,이왕석,010-8291-3846,,site-okjeong-sewage,,부장,,,HM02750,king3846@hallasanup.com
swkim@hallasanup.com,김성원,011-9612-3648,,site-okjeong-sewage,,부장,,,HM02114,swkim@hallasanup.com
jhlee3@hallasanup.com,이정현,010-4059-2983,,site-okjeong-sewage,,부장,,,HM02696,leejh1978@hallasanup.com
jslim@hallasanup.com,임진수,010-3330-6379,,ops-onsan-bio,,부장,,,HM00563,7878lim@hallasanup.com
mjsong@hallasanup.com,송미정,010-9617-0424,,ops-onsan-bio,,과장,,,HF02776,mjsong@hallasanup.com
hdcho@hallasanup.com,조효덕,010-6231-8878,,site-onsan-sewage,,부장,,,HM01956,hyodeok@hallasanup.com
sbroh@hallasanup.com,노승복,010-8556-7193,,site-onsan-sewage,,부장,,,HM02527,n2316@hallasanup.com
hjchoi@hallasanup.com,최호진,010-9934-6379,,site-onsan-sewage,,과장,,,HM02668,chjmn1@hallasanup.com
ycmoon@hallasanup.com,문영철,010-3694-6212,,site-onsan-sewage,,부장,,,HM02738,
hcchu@hallasanup.com,추현철,010-6611-0147,,site-onsan-sewage,,부장,,,HM02633,chc0147@hallasanup.com
ylko@hallasanup.com,고영일,010-3371-9853,,site-onsan-sewage,,부장,,,HM02686,kyi9404@hallasanup.com
jykim1@hallasanup.com,김준연,010-7568-7712,,site-onsan-sewage,,차장,,,HM02701,jybarara1013@hallasanup.com
sjan@hallasanup.com,안석진,010-9172-9466,,site-onsan-sewage,,부장,,,HM02706,jinni4004@hallasanup.com
trkim@hallasanup.com,김태림,010-4764-0601,,site-onsan-sewage,,사원,,,HF02770,sksek80@hallasanup.com
hjlim@hallasanup.com,임해중,010-3582-7323,,hanlla-operations-office,,전무이사,,,HM00152,lhj@hallasanup.com
scshin@hallasanup.com,신성철,010-9037-5830,,hanlla-operations-office,,부장,,,HM01146,thrkr2@hallasanup.com
mskim@hallasanup.com,김민수,010-6760-7435,,ops-ulsan-incineration,,상무이사,,,HM00061,mskim@hallasanup.com
kokim@hallasanup.com,김노은,010-3139-6341,,ops-ulsan-incineration,,과장,,,HF00468,kko1665@hallasanup.com
dhyang@hallasanup.com,양도영,010-6695-7083,,site-jangnyang-sewage,,차장,,,HM02651,yangdo10@hallasanup.com
jochoi@hallasanup.com,최정의,010-6617-8946,,site-jangnyang-sewage,,과장,,,HM02525,tmzdk23@hallasanup.com
jklee@hallasanup.com,이재광,010-9398-6830,,site-jangnyang-sewage,,이사,,,HM01963,ljk3030@hallasanup.com
wshwang@hallasanup.com,황운식,010-4598-6225,,site-jangnyang-sewage,,부장,,,HM02634,hwsaaa@hallasanup.com
hsson@hallasanup.com,손현수,010-3836-4155,,site-jangnyang-sewage,,과장,,,HM02685,happy09005@hallasanup.com
jalee@hallasanup.com,이지애1,010-3521-2095,,site-jangnyang-sewage,,사원,,,HF02702,jiae82@hallasanup.com
skkang@hallasanup.com,강신규,010-3684-4964,,hanlla-env-project-mgmt,,부장,,,HM01966,skang@hallasanup.com
hykang@hallasanup.com,강화영,010-8838-1886,,hanlla-env-project-mgmt,,과장,,,HM02625,kanghwayoung@hallasanup.com
khkim1@hallasanup.com,김경한,010-4620-1183,,hanlla-env-plant-design,,상무이사,,,HM01832,khankim@hallasanup.com
1 email name phone role tenant_slug department grade position jobTitle employee_id sub_email
2 khkim@hallasanup.com 김광현 010-7411-4180 hanlla-mgmt-support-hq 전무이사 HM00112 ezfree@hallasanup.com
3 dcsong@hallasanup.com 송동찬 010-5414-6973 hanlla-mgmt-support 이사 HM02597 sdc0327@hallasanup.com
4 jhlee@hallasanup.com 이준하 010-9189-9705 hanlla-mgmt-support 차장 HM01357 jhlee@hallasanup.com
5 wspark@hallasanup.com 박우식 010-4851-8193 hanlla-mgmt-support 상무이사 HM00417 pws9@hallasanup.com
6 islee@hallasanup.com 이인서 010-5405-8044 hanlla-business-support 부장 HM02581 islee@hallasanup.com
7 kaseo@hallasanup.com 서경아 010-5242-9794 hanlla-business-support 차장 HF00442 sky@hallasanup.com
8 jhlee1@hallasanup.com 이종희 010-8626-8710 hanlla-operations 차장 HM02420 jhlee01@hallasanup.com
9 bjyoo@hallasanup.com 유붕종 010-4519-2634 hanlla-general-business 전무이사 HM01775 yoo660327@hallasanup.com
10 hckim@hallasanup.com 김희철 010-3790-0273 hanlla-operations 부장 HM00564 mecha011@hallasanup.com
11 jhchoi@hallasanup.com 최재혁 010-8831-7961 site-gwangju-wastewater 부장 HM02713 jh7184@hallasanup.com
12 hjjang@hallasanup.com 장홍재 010-5024-0588 site-gwangju-wastewater 차장 HM02747 hjjang@hallasanup.com
13 cwpark@hallasanup.com 박찬웅 010-2519-8190 site-gwangju-wastewater 대리 HM02643 fpeld15@hallasanup.com
14 jwlee@hallasanup.com 이정우 010-3729-2726 site-gwangju-wastewater 부장 HM02624 ljw1069@hallasanup.com
15 yjkang@hallasanup.com 강영진1 010-9254-4559 site-gwangju-wastewater 부장 HM02553 bukzy@hallasanup.com
16 swchoi@hallasanup.com 최승우 010-3816-5053 site-gwangju-wastewater 부장 HM02735 paikiki@hallasanup.com
17 dkkim@hallasanup.com 김동균1 010-9125-6361 site-gwangju-wastewater 부장 HM02697 kdk73@hallasanup.com
18 yjlee@hallasanup.com 이여진 010-2349-8106 site-gwangju-wastewater 사원 HF02728 yeojin@hallasanup.com
19 bmlee@hallasanup.com 이병문 010-3565-4737 hanlla-env-plant-hq 전무이사 HM00056 bmlee@hallasanup.com
20 syyang@hallasanup.com 양승엽 010-6246-7872 hanlla-infra-project-mgmt 상무이사 HM01976 ysungyup@hallasanup.com
21 dhkim@hallasanup.com 김동현 010-2463-5874 hanlla-infra-project-mgmt 부장 HM02775 kdh0202@hallasanup.com
22 sjkim@hallasanup.com 김승주 010-2929-1268 hanlla-infra-project-mgmt 대리 HM02678 ksj@hallasanup.com
23 skyang@hallasanup.com 양승경 010-2753-4994 hanlla-infra-project-mgmt 과장 HF01029 tmdrud86@hallasanup.com
24 djkwon@hallasanup.com 권대준 010-2458-3373 hanlla-general-sales 전무이사 HM02570 jjoony@hallasanup.com
25 wgkim@hallasanup.com 김원근 010-7634-7872 hanlla-executive 사장 HM01774 kimwg7872@hallasanup.com
26 btkim@hallasanup.com 김범태 010-4653-5690 site-docheok-silchon-road 부장 HM02722 70kbt@hallasanup.com
27 skhong@hallasanup.com 홍성관 010-9458-1766 site-docheok-silchon-road 차장 HM02719 kwany@hallasanup.com
28 dkchoi@hallasanup.com 최덕규 010-3315-5579 site-docheok-silchon-road 차장 HM02718 cdk20001@hallasanup.com
29 jhkim3@hallasanup.com 김종호 010-2758-9606 site-docheok-silchon-road 부장 HM02715 civilman12@hallasanup.com
30 jmkoo@hallasanup.com 구자민 010-5057-3073 site-docheok-silchon-road 대리 HM02721 wkals4582@hallasanup.com
31 khpark@hallasanup.com 박기황 010-2527-4607 site-docheok-silchon-road 부장 HM02717 parkkhhh@hallasanup.com
32 ehpark@hallasanup.com 박은호 010-9740-6634 site-docheok-silchon-road 차장 HM02734 eunho6634@hallasanup.com
33 kjkoo@hallasanup.com 구교진 010-3151-2660 site-docheok-silchon-road 사원 HM02638 kkj0509@hallasanup.com
34 shhong@hallasanup.com 홍순화 010-6624-6733 site-docheok-silchon-road 사원 HF02737 hongga328@hallasanup.com
35 dhlee@hallasanup.com 이동훈 010-3427-8276 site-busan-new-port 부장 HM02542 dhlee88@hallasanup.com
36 jgsong@hallasanup.com 송재규 010-9282-8213 site-busan-new-port 차장 HM02308 jgsong@hallasanup.com
37 wkjung@hallasanup.com 정원근 010-8923-2936 site-busan-new-port 부장 HM00365 jwg2936@hallasanup.com
38 syseok@hallasanup.com 석성용2 010-5654-5175 site-busan-new-port 차장 HM02644 ssssam4080@hallasanup.com
39 nrha@hallasanup.com 하누리 010-3005-5453 site-busan-new-port 차장 HM01015 nurry@hallasanup.com
40 jsjun@hallasanup.com 전종식2 010-2285-2324 site-busan-new-port 부장 HM02736 shik2324@hallasanup.com
41 rklee@hallasanup.com 이령경 010-5747-0944 site-busan-new-port 사원 HF02761 kyeong951104@hallasanup.com
42 hslee@hallasanup.com 이한소 010-8820-4650 site-busan-new-port 과장 HM02760 lhso111@hallasanup.com
43 bslee@hallasanup.com 이병석 010-4520-5573 site-busan-new-port 부장 HM02766 hopesuck20@hallasanup.com
44 smpark@hallasanup.com 박성민3 010-3631-8923 site-busan-new-port 이사 HM02780 smpark@hallasanup.com
45 jichoi@hallasanup.com 최재인 010-2205-0870 site-bucheon-gulpocheon 차장 HM02511 dia044653@hallasanup.com
46 ytyoo@hallasanup.com 유연태 010-8735-1567 site-bucheon-gulpocheon 부장 HM02730 yyt1567@hallasanup.com
47 hjkim@hallasanup.com 김희준 010-6288-8751 site-bucheon-gulpocheon 부장 HM02496 s09j15e@hallasanup.com
48 hssong@hallasanup.com 송홍섭 010-8963-1595 site-bucheon-gulpocheon 차장 HM02714 SH1029@hallasanup.com
49 mgkong@hallasanup.com 공민구 010-7752-8755 site-bucheon-gulpocheon 대리 HM02603 kongkevin@hallasanup.com
50 jhkim2@hallasanup.com 김진혁1 010-3708-8916 site-bucheon-gulpocheon 사원 HM02711 jhk752@hallasanup.com
51 shshin@hallasanup.com 신상훈 010-9140-8549 site-bucheon-gulpocheon 부장 HM02748 sshun1105@hallasanup.com
52 ksmoon@hallasanup.com 문경수 010-5435-8013 site-bucheon-gulpocheon 부장 HM02773 mks@hallasanup.com
53 silee@hallasanup.com 이송이 010-8923-8029 site-bucheon-gulpocheon 사원 HF02693 lovelyanne87@hallasanup.com
54 hyjeong@hallasanup.com 정호영 010-8596-4936 site-sudokwon-landfill-2 과장 HM01990 jhymhn@hallasanup.com
55 shcho@hallasanup.com 조성훈1 010-3999-9502 site-sudokwon-landfill-2 부장 HM02534 hoon96@hallasanup.com
56 mkryu@hallasanup.com 류문경 010-9489-6975 site-sudokwon-landfill-2 부장 HM02751 ryu9643019@hallasanup.com
57 shhan@hallasanup.com 한성호 010-9393-7097 site-sudokwon-landfill-2 부장 HM02763 hsh7097@hallasanup.com
58 syoh@hallasanup.com 오세영 010-2868-8953 site-sudokwon-landfill-2 이사 HM01969 Osy8953530@hallasanup.com
59 yssong@hallasanup.com 송영석 010-2731-6730 site-sudokwon-landfill-2 부장 HM02517 sys6730@hallasanup.com
60 dwlee@hallasanup.com 이대원 010-2313-9963 site-sudokwon-landfill-2 차장 HM02472 leedw@hallasanup.com
61 rrpark@hallasanup.com 박루리 010-9146-9934 site-sudokwon-landfill-2 사원 HF02774 parkruri@hallasanup.com
62 pkson@hallasanup.com 손판국 010-9367-5967 site-sincheon-sewage 부장 HM02563 sagibry@hallasanup.com
63 mjlee@hallasanup.com 이명준 010-6399-5159 site-sincheon-sewage 차장 HM02491 myungjoon@hallasanup.com
64 bspark@hallasanup.com 박봉식 010-4009-2670 site-sincheon-sewage 과장 HM02733 bspark1216@hallasanup.com
65 mhbeak@hallasanup.com 백무현 010-2235-6749 site-sincheon-sewage 부장 HM02742 hyeonoo@hallasanup.com
66 jwnam@hallasanup.com 남준우 010-6305-6648 site-sincheon-sewage 대리 HM02755 nju6305200@hallasanup.com
67 jklee1@hallasanup.com 이중곤 010-7166-5994 site-sincheon-sewage 차장 HM02779 wndwkdia0@hallasanup.com
68 hjlee@hallasanup.com 이현진 010-4858-5229 site-sincheon-sewage 대리 HM02732 hjlee@hallasanup.com
69 suan@hallasanup.com 안소은 010-7761-5241 site-sincheon-sewage 사원 HF02746 an016363@hallasanup.com
70 jwjung@hallasanup.com 정재욱 010-8543-5111 site-apo-sewage 부장 HM02727 wjdwodnr77@hallasanup.com
71 ybkim@hallasanup.com 김유빈 010-4112-2647 site-apo-sewage 사원 HM02596 ybkim@hallasanup.com
72 smjoo@hallasanup.com 주성민 010-9938-8420 site-apo-sewage 과장 HM02547 jsm@hallasanup.com
73 jhkim@hallasanup.com 김제희 010-2703-0072 site-apo-sewage 부장 HM02551 yeskjh72@hallasanup.com
74 dyshin@hallasanup.com 신동엽 010-2523-3364 site-apo-sewage 부장 HM01968 shindongyop@hallasanup.com
75 hkjang@hallasanup.com 장현관 010-5195-0348 site-apo-sewage 차장 HM02544 eddy2001@hallasanup.com
76 hglee@hallasanup.com 이희곤 010-5358-1021 site-apo-sewage 부장 HM02622 lhg1021@hallasanup.com
77 jhkim1@hallasanup.com 김정훈1 010-5275-8622 site-apo-sewage 차장 HM02710 jhk7510@hallasanup.com
78 tylee@hallasanup.com 이태영 010-2805-2836 site-apo-sewage 부장 HM00813 black0112@hallasanup.com
79 syjeon@hallasanup.com 전소영 010-9600-2199 site-apo-sewage 사원 HF02560 jsy2220@hallasanup.com
80 hykwon@hallasanup.com 권해용1 010-6522-8153 ops-anseong-wwtp 부장 HM02507 khy11k@hallasanup.com
81 jyher@hallasanup.com 허지연 010-2735-5266 ops-anseong-wwtp 과장 HF02744 jyheo@hallasanup.com
82 gykim@hallasanup.com 김기영 010-5399-2520 hanlla-safety-hq 전무이사 HM00126 young@hallasanup.com
83 wjyun@hallasanup.com 윤원종 010-8243-8083 hanlla-safety-team 사원 HM02740 yunwj0808@hallasanup.com
84 ddkang@hallasanup.com 강대득 010-8738-9165 site-yeoju-bupyeongcheon 부장 HM02504 kdd3880@hallasanup.com
85 hypark@hallasanup.com 박흥열 010-2312-9853 site-yeoju-bupyeongcheon 과장 HM02749 as8742@hallasanup.com
86 kyryu@hallasanup.com 류길용 010-2621-6152 site-yeoju-bupyeongcheon 과장 HM02741 bbangrky@hallasanup.com
87 sklee@hallasanup.com 이상규 010-8650-0875 site-yeoju-bupyeongcheon 차장 HM02762 dorajisa@hallasanup.com
88 jhjung@hallasanup.com 정지호 010-9013-9596 site-yeoju-bupyeongcheon 부장 HM02765 need1971@hallasanup.com
89 jykim@hallasanup.com 김지용 010-5685-7038 site-okjeong-sewage 차장 HM02610 gmn1038@hallasanup.com
90 jschoi@hallasanup.com 최종순 010-2890-0856 site-okjeong-sewage 차장 HM02505 ddjjs@hallasanup.com
91 wjkim@hallasanup.com 김원준 010-3194-3222 site-okjeong-sewage 과장 HM02422 sirio@hallasanup.com
92 djpark@hallasanup.com 박대중 010-6561-5255 site-okjeong-sewage 부장 HM02689 ptgsun1@hallasanup.com
93 wslee@hallasanup.com 이왕석 010-8291-3846 site-okjeong-sewage 부장 HM02750 king3846@hallasanup.com
94 swkim@hallasanup.com 김성원 011-9612-3648 site-okjeong-sewage 부장 HM02114 swkim@hallasanup.com
95 jhlee3@hallasanup.com 이정현 010-4059-2983 site-okjeong-sewage 부장 HM02696 leejh1978@hallasanup.com
96 jslim@hallasanup.com 임진수 010-3330-6379 ops-onsan-bio 부장 HM00563 7878lim@hallasanup.com
97 mjsong@hallasanup.com 송미정 010-9617-0424 ops-onsan-bio 과장 HF02776 mjsong@hallasanup.com
98 hdcho@hallasanup.com 조효덕 010-6231-8878 site-onsan-sewage 부장 HM01956 hyodeok@hallasanup.com
99 sbroh@hallasanup.com 노승복 010-8556-7193 site-onsan-sewage 부장 HM02527 n2316@hallasanup.com
100 hjchoi@hallasanup.com 최호진 010-9934-6379 site-onsan-sewage 과장 HM02668 chjmn1@hallasanup.com
101 ycmoon@hallasanup.com 문영철 010-3694-6212 site-onsan-sewage 부장 HM02738
102 hcchu@hallasanup.com 추현철 010-6611-0147 site-onsan-sewage 부장 HM02633 chc0147@hallasanup.com
103 ylko@hallasanup.com 고영일 010-3371-9853 site-onsan-sewage 부장 HM02686 kyi9404@hallasanup.com
104 jykim1@hallasanup.com 김준연 010-7568-7712 site-onsan-sewage 차장 HM02701 jybarara1013@hallasanup.com
105 sjan@hallasanup.com 안석진 010-9172-9466 site-onsan-sewage 부장 HM02706 jinni4004@hallasanup.com
106 trkim@hallasanup.com 김태림 010-4764-0601 site-onsan-sewage 사원 HF02770 sksek80@hallasanup.com
107 hjlim@hallasanup.com 임해중 010-3582-7323 hanlla-operations-office 전무이사 HM00152 lhj@hallasanup.com
108 scshin@hallasanup.com 신성철 010-9037-5830 hanlla-operations-office 부장 HM01146 thrkr2@hallasanup.com
109 mskim@hallasanup.com 김민수 010-6760-7435 ops-ulsan-incineration 상무이사 HM00061 mskim@hallasanup.com
110 kokim@hallasanup.com 김노은 010-3139-6341 ops-ulsan-incineration 과장 HF00468 kko1665@hallasanup.com
111 dhyang@hallasanup.com 양도영 010-6695-7083 site-jangnyang-sewage 차장 HM02651 yangdo10@hallasanup.com
112 jochoi@hallasanup.com 최정의 010-6617-8946 site-jangnyang-sewage 과장 HM02525 tmzdk23@hallasanup.com
113 jklee@hallasanup.com 이재광 010-9398-6830 site-jangnyang-sewage 이사 HM01963 ljk3030@hallasanup.com
114 wshwang@hallasanup.com 황운식 010-4598-6225 site-jangnyang-sewage 부장 HM02634 hwsaaa@hallasanup.com
115 hsson@hallasanup.com 손현수 010-3836-4155 site-jangnyang-sewage 과장 HM02685 happy09005@hallasanup.com
116 jalee@hallasanup.com 이지애1 010-3521-2095 site-jangnyang-sewage 사원 HF02702 jiae82@hallasanup.com
117 skkang@hallasanup.com 강신규 010-3684-4964 hanlla-env-project-mgmt 부장 HM01966 skang@hallasanup.com
118 hykang@hallasanup.com 강화영 010-8838-1886 hanlla-env-project-mgmt 과장 HM02625 kanghwayoung@hallasanup.com
119 khkim1@hallasanup.com 김경한 010-4620-1183 hanlla-env-plant-design 상무이사 HM01832 khankim@hallasanup.com

306
works_users_hanmac.CSV Normal file
View File

@@ -0,0 +1,306 @@
,email,name,phone,role,tenant_slug,,department,grade,position,jobTitle,employee_id,sub_email
교통부,sskim6@hanmaceng.co.kr,김상수,010-5216-9660,user,traffic,,,전무,부서장,,M12203,sskim@hanmaceng.co.kr
,yjkim8@hanmaceng.co.kr,김용종,010-6721-0814,user,traffic,,,상무,,,M21445,topyj@hanmaceng.co.kr
,kkimyk@hanmaceng.co.kr,김영권,010-6405-1797,user,traffic,,,상무,,,M02259,kkimyk@hanmaceng.co.kr
,yheo@hanmaceng.co.kr,허윤,010-2569-1504,user,traffic,,,과장,,,M25042,m25042@hanmaceng.co.kr
,jhkim12@hanmaceng.co.kr,김준형,010-2623-9508,user,traffic,,,대리,,,M21435,m21435@hanmaceng.co.kr
,siyoon@hanmaceng.co.kr,윤선일,010-4353-7600,user,traffic,,,대리,,,M23010,m23010@hanmaceng.co.kr
,nhlee@hanmaceng.co.kr,이난희,010-4398-3410,user,traffic,,,사원,,,M25044,m25044@hanmaceng.co.kr
,shmun@hanmaceng.co.kr,문수하,010-5648-0314,user,traffic,,,사원,,,M25068,m25068@hanmaceng.co.kr
구조부,ykshin2@hanmaceng.co.kr,신영각,010-3290-6057,user,infra-structures,,,부사장,,,M16208,twindol@hanmaceng.co.kr
,shyoon@hanmaceng.co.kr,윤성호,010-5286-8476,user,infra-structures,,,부사장,부서장,,M10103,feel365@hanmaceng.co.kr
,jboh@hanmaceng.co.kr,오재범,010-9173-3560,user,infra-structures,,,전무,,,M03202,jbeom@hanmaceng.co.kr
,jykim9@hanmaceng.co.kr,김재용,010-9627-8774,user,infra-structures,,,상무,,,M03228,kjy730@hanmaceng.co.kr
,shhwang@hanmaceng.co.kr,황승현,010-8727-8306,user,infra-structures,,,상무,,,M02308,hshksy7367@hanmaceng.co.kr
,shlee23@hanmaceng.co.kr,이세화,010-9479-2199,user,infra-structures,,,부장,,,M18204,shlee81@hanmaceng.co.kr
,dskim5@hanmaceng.co.kr,김대성,010-8709-6322,user,infra-structures,,,차장,,,M24016,m24016@hanmaceng.co.kr
,sdkim4@hanmaceng.co.kr,김성도,010-5191-2520,user,infra-structures,,,과장,,,M21439,m21439@hanmaceng.co.kr
,jmsong@hanmaceng.co.kr,송재명,010-2393-5797,user,infra-structures,,,과장,,,M24017,m24017@hanmaceng.co.kr
,ohjeon@hanmaceng.co.kr,전오현,010-2464-6231,user,infra-structures,,,과장,,,M25067,m25067@hanmaceng.co.kr
,ycchoi@hanmaceng.co.kr,최영철,010-9746-4146,user,infra-structures,,,과장,,,M25018,m25018@hanmaceng.co.kr
,jmhwang2@hanmaceng.co.kr,황지만,010-2235-5634,user,infra-structures,,,과장,,,M23042,m23042@hanmaceng.co.kr
,twkim2@hanmaceng.co.kr,김태욱,010-6577-1853,user,infra-structures,,,대리,,,M23006,m23006@hanmaceng.co.kr
,jkban@hanmaceng.co.kr,반진관,010-2677-4083,user,infra-structures,,,대리,,,M21407,m21407@hanmaceng.co.kr
,mjoh@hanmaceng.co.kr,오민종,010-8258-3400,user,infra-structures,,,대리,,,M23054,m23054@hanmaceng.co.kr
,mkjeon@hanmaceng.co.kr,전민기,010-9945-0295,user,infra-structures,,,대리,,,M24011,m24011@hanmaceng.co.kr
,jmson@hanmaceng.co.kr,손지민,010-5195-8257,user,infra-structures,,,사원,,,M22055,m22055@hanmaceng.co.kr
,wsshim@hanmaceng.co.kr,심우석,010-7548-7885,user,infra-structures,,,사원,,,M25017,m25017@hanmaceng.co.kr
지반,hmlee3@hanmaceng.co.kr,이한민,010-3255-9884,user,infra-geotech-tunnel,,,상무,부서장,,M20105,fastmin@hanmaceng.co.kr
,jilee2@hanmaceng.co.kr,이종익,010-3869-3451,user,infra-geotech-tunnel,,,상무,,,M07223,jongiklee@hanmaceng.co.kr
,sbsim@hanmaceng.co.kr,심성보,010-7242-2931,user,infra-geotech-tunnel,,,이사,,,M24036,m24036@hanmaceng.co.kr
,jkyu@hanmaceng.co.kr,유재극,010-8227-0078,user,infra-geotech-tunnel,,,이사,,,M20211,visyase@hanmaceng.co.kr
,tskim2@hanmaceng.co.kr,김태식,010-9822-8592,user,infra-geotech-tunnel,,,차장,,,M21451,m21451@hanmaceng.co.kr
,mskang2@hanmaceng.co.kr,강민수,010-7933-0183,user,infra-geotech-tunnel,,,과장,,,M25021,m25021@hanmaceng.co.kr
,sbpark2@hanmaceng.co.kr,박상빈,010-8281-7928,user,infra-geotech-tunnel,,,과장,,,M23076,m23076@hanmaceng.co.kr
,mjsong2@hanmaceng.co.kr,송민제,010-7268-3500,user,infra-geotech-tunnel,,,과장,,,M24075,m22022@hanmaceng.co.kr
,swpark4@hanmaceng.co.kr,박선우,010-4823-7458,user,infra-geotech-tunnel,,,대리,,,M20321,m20321@hanmaceng.co.kr
,sjjeon@hanmaceng.co.kr,전수진,010-3791-6816,user,infra-geotech-tunnel,,,대리,,,M21468,m21468@hanmaceng.co.kr
,jtkim2@hanmaceng.co.kr,김진토,010-9370-4673,user,infra-geotech-tunnel,,,사원,,,M26014,m26014@hanmaceng.co.kr
도시,cgkim@hanmaceng.co.kr,김춘근,010-3354-8398,user,land-env-urban-planning,,,전무,,,M21413,m21413@hanmaceng.co.kr
,hyjeong2@hanmaceng.co.kr,정혜연,010-2391-2158,user,land-env-urban-planning,,,전무,부서장,,M14101,myisland@hanmaceng.co.kr
,hhshin@hanmaceng.co.kr,신현호,010-9316-2217,user,land-env-urban-planning,,,상무,,,M21458,m21458@hanmaceng.co.kr
,dynam@hanmaceng.co.kr,남동윤,010-5416-1408,user,land-env-urban-planning,,,이사,,,M21472,m21472@hanmaceng.co.kr
,hkchoi2@hanmaceng.co.kr,최현근,010-6205-2355,user,land-env-urban-planning,,,이사,,,M24045,m24045@hanmaceng.co.kr
,gwkim2@hanmaceng.co.kr,김건우,010-9324-5023,user,land-env-urban-planning,,,부장,,,M14301,kgenu@hanmaceng.co.kr
,jwshin3@hanmaceng.co.kr,신진우,010-3434-2949,user,land-env-urban-planning,,,부장,,,M21414,m21414@hanmaceng.co.kr
,skryu@hanmaceng.co.kr,류성균,010-4211-5202,user,land-env-urban-planning,,,부장,,,M25022,m25022@hanmaceng.co.kr
,jkhwang@hanmaceng.co.kr,황재광,010-4780-0137,user,land-env-urban-planning,,,차장,,,M25050,m25050@hanmaceng.co.kr
,jwbae2@hanmaceng.co.kr,배진우,010-7501-1272,user,land-env-urban-planning,,,과장,,,M24073,m24073@hanmaceng.co.kr
,yjlee7@hanmaceng.co.kr,이예지,010-9937-1276,user,land-env-urban-planning,,,과장,,,M21462,m21462@hanmaceng.co.kr
,dhchoi@hanmaceng.co.kr,최대헌,010-2214-9153,user,land-env-urban-planning,,,과장,,,M24038,m24038@hanmaceng.co.kr
,ghkim@hanmaceng.co.kr,김관후,010-6302-3157,user,land-env-urban-planning,,,대리,,,M22051,m22051@hanmaceng.co.kr
,mssong@hanmaceng.co.kr,송미성,010-6258-4972,user,land-env-urban-planning,,,대리,,,M25027,m25027@hanmaceng.co.kr
,shshin@hanmaceng.co.kr,신송희,010-9518-4988,user,land-env-urban-planning,,,대리,,,M21422,m21422@hanmaceng.co.kr
,hjchae@hanmaceng.co.kr,채호주,010-9038-8389,user,land-env-urban-planning,,,대리,,,M23070,m23070@hanmaceng.co.kr
,wskim4@hanmaceng.co.kr,김원섭,010-9117-7449,user,land-env-urban-planning,,,사원,,,M24062,m24062@hanmaceng.co.kr
,jeeim@hanmaceng.co.kr,임지은,010-3994-3634,user,land-env-urban-planning,,,사원,,,M25047,m25047@hanmaceng.co.kr
수자,HGLEE4@hanmaceng.co.kr,이홍규,010-6201-6542,user,land-env-water-resources,,,부사장,,,M19103,hglee@hanmaceng.co.kr
,JCHYUN@hanmaceng.co.kr,현종철,010-6204-3628,user,land-env-water-resources,,,전무,부서장,,M21410,m21410@hanmaceng.co.kr
,JJLEE4@hanmaceng.co.kr,이재진,010-2470-7923,user,land-env-water-resources,,,이사,,,M08206,ljj7924@hanmaceng.co.kr
,BHLEE@hanmaceng.co.kr,이병화,010-9133-5068,user,land-env-water-resources,,,이사,,,M21428,m21428@hanmaceng.co.kr
,SCSEO@hanmaceng.co.kr,서순창,010-9899-8042,user,land-env-water-resources,,,부장,,,M21425,m21425@hanmaceng.co.kr
,JBYOO@hanmaceng.co.kr,유재범,010-7100-7942,user,land-env-water-resources,,,차장,,,M16308,jboom@hanmaceng.co.kr
,DYKIM@hanmaceng.co.kr,김덕용,010-6235-5976,user,land-env-water-resources,,,차장,,,M22058,m22058@hanmaceng.co.kr
,SHHYUN@hanmaceng.co.kr,현석훈,010-9932-0236,user,land-env-water-resources,,,차장,,,M21454,m21454@hanmaceng.co.kr
,YHHWANG2@hanmaceng.co.kr,황윤희,010-8484-8721,user,land-env-water-resources,,,과장,,,M24025,m24025@hanmaceng.co.kr
,SHJUNG@hanmaceng.co.kr,정승훈,010-5192-8290,user,land-env-water-resources,,,대리,,,M25012,m25012@hanmaceng.co.kr
,MSKIM13@hanmaceng.co.kr,김명식,010-2538-4933,user,land-env-water-resources,,,대리,,,M22008,myungsik@hanmaceng.co.kr
,DBLEE@hanmaceng.co.kr,이다빈,010-9738-1325,user,land-env-water-resources,,,대리,,,M25011,m25011@hanmaceng.co.kr
,WJCHO@hanmaceng.co.kr,조우진,010-3327-0817,user,land-env-water-resources,,,대리,,,M23058,m23058@hanmaceng.co.kr
상하,sspark3@hanmaceng.co.kr,박승신,010-2272-1436,user,water-sewerage,,,부사장,부서장,,M22009,m22009@hanmaceng.co.kr
,dwlee4@hanmaceng.co.kr,이동욱,010-6339-1959,user,water-sewerage,,,전무이사,,,M20104,quaaahoo@hanmaceng.co.kr
,ywgil@hanmaceng.co.kr,길이원,010-9917-0157,user,water-sewerage,,,차장,,,M20217,m20217@hanmaceng.co.kr
,dwkim5@hanmaceng.co.kr,김동욱,010-7705-7489,user,water-sewerage,,,과장,,,M26010,m26010@hanmaceng.co.kr
,sjkim10@hanmaceng.co.kr,김성주,010-3885-8068,user,water-sewerage,,,과장,,,M25058,m25058@hanmaceng.co.kr
,yjim@hanmaceng.co.kr,임연재,010-4223-3881,user,water-sewerage,,,대리,,,M22066,m22066@hanmaceng.co.kr
,bkkim@hanmaceng.co.kr,김병국,010-9303-3245,user,water-sewerage,,,대리,,,M26003,m26003@hanmaceng.co.kr
,yhlim@hanmaceng.co.kr,임유화,010-8383-3522,user,water-sewerage,,,사원,,,M23038,m23038@hanmaceng.co.kr
,ybsim@hanmaceng.co.kr,심윤보,010-5125-3016,user,water-sewerage,,,사원,,,M24065,m24065@hanmaceng.co.kr
,dhhwang2@hanmaceng.co.kr,황동훈,010-2505-1873,user,water-sewerage,,,사원,,,M25043,m25043@hanmaceng.co.kr
안전,sako@hanmaceng.co.kr,고삼암,010-5361-7995,user,safety-diagnosis,,,부회장,,,M04503,ksam51@hanmaceng.co.kr
,hsbyoun@hanmaceng.co.kr,변한석,010-6641-9995,user,safety-diagnosis,,,전무,부서장,,M22057,m22057@hanmaceng.co.kr
,kjlee2@hanmaceng.co.kr,이기종,010-3899-9830,user,safety-diagnosis,,,전무,,,M06505,lkj0997@hanmaceng.co.kr
,jsjo@hanmaceng.co.kr,조증식,010-5350-6952,user,safety-diagnosis,,,전무,,,M23081,m23081@hanmaceng.co.kr
,hclim@hanmaceng.co.kr,임현철,010-8749-9448,user,safety-diagnosis,,,상무,,,M21455,m21455@hanmaceng.co.kr
,jyson2@hanmaceng.co.kr,손재용,010-9009-8961,user,safety-diagnosis,,,이사,,,M08306,son0802@hanmaceng.co.kr
,hmyeon@hanmaceng.co.kr,연훈모,010-7923-8703,user,safety-diagnosis,,,차장,,,M24029,yhm8703@hanmaceng.co.kr
,sgjeon2@hanmaceng.co.kr,전성구,010-9199-8060,user,safety-diagnosis,,,차장,,,M21429,m21429@hanmaceng.co.kr
,ejkim@hanmaceng.co.kr,김응진,010-9960-1522,user,safety-diagnosis,,,과장,,,M21457,m21457@hanmaceng.co.kr
,tykim3@hanmaceng.co.kr,김태연,010-7171-5261,user,safety-diagnosis,,,과장,,,M20306,ququ44@hanmaceng.co.kr
,wjna@hanmaceng.co.kr,나원준,010-6652-8978,user,safety-diagnosis,,,과장,,,M25014,m25014@hanmaceng.co.kr
,gspark3@hanmaceng.co.kr,박건석,010-6225-0356,user,safety-diagnosis,,,과장,,,M25015,m25015@hanmaceng.co.kr
,ghpark4@hanmaceng.co.kr,박건희,010-6434-7402,user,safety-diagnosis,,,과장,,,M25016,m25016@hanmaceng.co.kr
,ghbeak@hanmaceng.co.kr,백건휘,010-8762-9098,user,safety-diagnosis,,,과장,,,M23001,m23001@hanmaceng.co.kr
,hcyun@hanmaceng.co.kr,윤현철,010-9604-4569,user,safety-diagnosis,,,과장,,,M23017,m23017@hanmaceng.co.kr
,jmlee3@hanmaceng.co.kr,이지명,010-4212-0867,user,safety-diagnosis,,,과장,,,M23014,m23014@hanmaceng.co.kr
,dvkim@hanmaceng.co.kr,김동빈,010-9417-1161,user,safety-diagnosis,,,대리,,,M25030,m25030@hanmaceng.co.kr
,tykim2@hanmaceng.co.kr,김태윤,010-2866-1853,user,safety-diagnosis,,,대리,,,M20312,xotls10@hanmaceng.co.kr
,cgyu@hanmaceng.co.kr,유찬근,010-2908-1628,user,safety-diagnosis,,,대리,,,M25059,m25059@hanmaceng.co.kr
,mjlee2@hanmaceng.co.kr,이명진,010-7577-6386,user,safety-diagnosis,,,대리,,,M24023,m24023@hanmaceng.co.kr
,whjeong@hanmaceng.co.kr,정원호,010-8470-3996,user,safety-diagnosis,,,대리,,,M25029,m25029@hanmaceng.co.kr
,jhkim34@hanmaceng.co.kr,김진호,010-4300-2625,user,safety-diagnosis,,,사원,,,M24022,m24022@hanmaceng.co.kr
,hopark@hanmaceng.co.kr,박현옥,010-4002-5162,user,safety-diagnosis,,,사원,,,M24084,m24084@hanmaceng.co.kr
,jsyu@hanmaceng.co.kr,유지성,010-9821-1155,user,safety-diagnosis,,,사원,,,M23037,m23037@hanmaceng.co.kr
,dhhan2@hanmaceng.co.kr,한동화,010-2054-4899,user,safety-diagnosis,,,과장,,,M25031,gksehdghk123@naver.com
,ychong@hanmaceng.co.kr,홍영철,010-9732-5097,user,safety-diagnosis,,,전무,,,M25032,hycoso@hanmail.net
임원실,khlee5@hanmaceng.co.kr,이경훈,010-3733-1890,user,hanmac,,,사장,대표이사,,M02107,khoonri@hanmaceng.co.kr
경영지원,sryou@hanmaceng.co.kr,유승열,010-5330-6503,user,sales-support,,,부사장,,,M10102,srryu65@hanmaceng.co.kr
,hwshin2@brsw.kr,신현우,010-8979-5720,user,baroncs,,,전무,부서장,,T02303,hwshin@hanmaceng.co.kr
,jhkim35@brsw.kr,김재헌,010-8785-2798,user,baroncs,,,상무,,,J08206,rurouno@hanmaceng.co.kr
,yioh@hanmaceng.co.kr,오윤익,010-4033-3566,user,경영지원부(SLUG값 없음),,,차장,,,M23040,m23040@hanmaceng.co.kr
,hmchae@hanmaceng.co.kr,채희문,010-9213-7842,user,경영지원부(SLUG값 없음),,,차장,,,M21321,m21321@hanmaceng.co.kr
,aylee@hanmaceng.co.kr,이애연,010-3356-0412,user,경영지원부(SLUG값 없음),,,과장,,,M25033,dodus1@hanmaceng.co.kr
,jwkim10@brsw.kr,김지우,010-4265-5624,user,baroncs,,,과장,,,T02328,jeewoo@hanmaceng.co.kr
,jhlee21@brsw.kr,이지혜,010-9528-0710,user,baroncs,,,과장,,,J14306,leejh@hanmaceng.co.kr
,jwyu@brsw.kr,유재욱,010-2819-1746,user,baroncs,,,과장,,,B20334,b20334@hanmaceng.co.kr
,jhlee22@brsw.kr,이준희,010-3200-6180,user,baroncs,,,과장,,,B18302,b18302@hanmaceng.co.kr
,hskim14@hanmaceng.co.kr,김현수,010-5020-4890,user,경영지원부(SLUG값 없음),,,과장,,,M22033,m22033@hanmaceng.co.kr
,dghyeon@hanmaceng.co.kr,현동규,010-4023-7476,user,경영지원부(SLUG값 없음),,,과장,,,B20305,hdg0310@hanmaceng.co.kr
,cwkim@hanmaceng.co.kr,김찬웅,010-6485-6536,user,경영지원부(SLUG값 없음),,,과장,,,M23007,ktts1234@hanmaceng.co.kr
,wsseo@hanmaceng.co.kr,서우석,010-8698-0757,user,경영지원부(SLUG값 없음),,,대리,,,M24046,m24046@hanmaceng.co.kr
,yjyoon@brsw.kr,윤영주,010-2802-4130,user,baroncs,,,대리,,,B14303,hanmaceml@hanmaceng.co.kr
,ymyu@hanmaceng.co.kr,유유민,010-3697-7320,user,경영지원부(SLUG값 없음),,,대리,,,M24013,m24013@hanmaceng.co.kr
,tsnoh@hanmaceng.co.kr,노태수,010-4819-9179,user,경영지원부(SLUG값 없음),,,대리,,,M22028,b21363@hanmaceng.co.kr
,yjkim26@hanmaceng.co.kr,김윤지,010-3299-8825,user,경영지원부(SLUG값 없음),,,대리,,,M22046,b21361@hanmaceng.co.kr
,egkim@hanmaceng.co.kr,김유진,010-8494-1087,user,경영지원부(SLUG값 없음),,,사원,,,M24069,m24069@hanmaceng.co.kr
,dhahn@hanmaceng.co.kr,안동현,010-4599-4397,user,경영지원부(SLUG값 없음),,,사원,,,M24040,m24040@hanmaceng.co.kr
,scyoon@brsw.kr,윤성찬,010-2873-3557,user,baroncs,,,사원,,,B21321,b21321@hanmaceng.co.kr
,hsoh@brsw.kr,오혜성,010-4067-9435,user,baroncs,,,사원,,,B21303,b21303@hanmaceng.co.kr
,jkchung@hanmaceng.co.kr,정준규,010-5346-6479,user,경영지원부(SLUG값 없음),,,사원,,,B25024,b25024@hanmaceng.co.kr
,wwkang@hanmaceng.co.kr,강우원,010-2317-0910,user,construction-business,,,부사장,,,M26021,m26021@hanmaceng.co.kr
,esko@hanmaceng.co.kr,고은식,010-3461-1654,user,construction-business,,,부사장,,,M25019,m25019@hanmaceng.co.kr
,jgkim1@hanmaceng.co.kr,김종계,010-3328-2408,user,construction-business,,,부사장,,,M24032,m24032@hanmaceng.co.kr
,ispark2@hanmaceng.co.kr,박익수,010-4531-8079,user,construction-business,,,부사장,,,M24041,m24041@hanmaceng.co.kr
,tkpark@hanmaceng.co.kr,박택규,010-7588-8558,user,construction-business,,,부사장,,,M21444,m21444@hanmaceng.co.kr
,ksshin@hanmaceng.co.kr,신광수,010-5228-4684,user,construction-management-h,,,부사장,본부장,,M04602,sks-1211@hanmaceng.co.kr
,hsshin2@hanmaceng.co.kr,신흥섭,010-3719-1271,user,construction-business,,,부사장,,,M23029,shs206066@hanmaceng.co.kr
,ksoh@hanmaceng.co.kr,오관식,010-9769-1005,user,construction-business,,,부사장,,,M23027,m23027@hanmaceng.co.kr
,bioh@hanmaceng.co.kr,오방일,010-2217-4907,user,construction-business,,,부사장,,,M24006,m24006@hanmaceng.co.kr
,islee2@hanmaceng.co.kr,이인상,010-4584-8504,user,construction-business,,,부사장,,,M21403,m21403@hanmaceng.co.kr
,smjeon@hanmaceng.co.kr,전성모,010-7422-2371,user,construction-business,,,부사장,,,M24058,m24058@hanmaceng.co.kr
,csham@hanmaceng.co.kr,함창수,010-2837-0372,user,construction-business,,,부사장,,,M23085,m23085@hanmaceng.co.kr
,mhkang@hanmaceng.co.kr,강명호,010-2019-2252,user,construction-business,,,전무이사,,,M16514,kmh6601@hanmaceng.co.kr
,mhkang2@hanmaceng.co.kr,강민호,010-3415-5033,user,construction-business,,,전무이사,,,M26031,m26031@hanmaceng.co.kr
,gwgo@hanmaceng.co.kr,고광욱,010-5358-7394,user,construction-business,,,전무이사,,,M06501,kokw6489@hanmaceng.co.kr
,ydkoh@hanmaceng.co.kr,고영덕,010-5488-7809,user,construction-business,,,전무이사,,,M24056,m24056@hanmaceng.co.kr
,thkwon1@hanmaceng.co.kr,권태훈,010-7266-8933,user,construction-business,,,전무이사,,,M22036,m22036@hanmaceng.co.kr
,ghkim6@hanmaceng.co.kr,김기환,010-9363-8459,user,construction-business,,,전무이사,,,M23059,m23059@hanmaceng.co.kr
,djkim3@hanmaceng.co.kr,김동주,010-2630-9999,user,construction-business,,,전무이사,,,M02608,dhkdj@hanmaceng.co.kr
,djkim4@hanmaceng.co.kr,김동준,010-4459-3231,user,construction-business,,,전무이사,,,M21405,m21405@hanmaceng.co.kr
,bckim2@hanmaceng.co.kr,김병찬,010-8532-2050,user,construction-business,,,전무이사,,,M15408,chany66@hanmaceng.co.kr
,bykim@hanmaceng.co.kr,김봉용,010-8524-9039,user,construction-business,,,전무이사,,,M21401,m21401@hanmaceng.co.kr
,sdkim5@hanmaceng.co.kr,김석동,010-5388-6687,user,construction-business,,,전무이사,,,M26033,m25023@hanmaceng.co.kr
,ygkim3@hanmaceng.co.kr,김양국,010-8545-3191,user,construction-business,,,전무이사,,,M16503,js030107@hanmaceng.co.kr
,yhkim2@hanmaceng.co.kr,김용혁,010-3225-6043,user,construction-business,,,전무이사,,,M21402,m21402@hanmaceng.co.kr
,wjkim2@hanmaceng.co.kr,김우진,010-3679-5287,user,construction-business,,,전무이사,,,M24039,kwj3431@hanmaceng.co.kr
,jhkim36@hanmaceng.co.kr,김재흥,010-8865-5830,user,construction-business,,,전무이사,,,M25071,m25071@hanmaceng.co.kr
,jgkim2@hanmaceng.co.kr,김정기,010-5765-3722,user,construction-business,,,전무이사,,,M23053,m23053@hanmaceng.co.kr
,chkim4@hanmaceng.co.kr,김찬호,010-5477-3482,user,construction-business,,,전무이사,,,M23052,m23052@hanmaceng.co.kr
,hjkim13@hanmaceng.co.kr,김형주,010-6809-2222,user,construction-business,,,전무이사,,,M24052,m24052@hanmaceng.co.kr
,htkim@hanmaceng.co.kr,김홍태,010-4544-6403,user,construction-business,,,전무이사,,,M16513,oklt0720@hanmaceng.co.kr
,ggnoh@hanmaceng.co.kr,노경국,010-8865-2211,user,construction-business,,,전무이사,,,M07313,rhodr@hanmaceng.co.kr
,diryu@hanmaceng.co.kr,류동일,010-2977-6245,user,construction-business,,,전무이사,,,M24076,m24076@hanmaceng.co.kr
,bymoon@hanmaceng.co.kr,문백용,010-3768-7918,user,construction-business,,,전무이사,,,M02606,alangback@hanmaceng.co.kr
,ihmin@hanmaceng.co.kr,민인홍,010-8752-0940,user,construction-business,,,전무이사,,,M24034,m24034@hanmaceng.co.kr
,ghpark@hanmaceng.co.kr,박광환,010-6553-4504,user,construction-business,,,전무이사,,,M26005,m26005@hanmaceng.co.kr
,mjpark@hanmaceng.co.kr,박명주,010-3702-0781,user,construction-business,,,전무이사,,,M25034,m25034@hanmaceng.co.kr
,mhpark2@hanmaceng.co.kr,박명희,010-9500-6598,user,construction-business,,,전무이사,,,M26006,m26006@hanmaceng.co.kr
,yspark5@hanmaceng.co.kr,박연수,010-3671-7768,user,construction-business,,,전무이사,,,M07507,pys3804@hanmaceng.co.kr
,jspark10@hanmaceng.co.kr,박종시,010-5375-7388,user,construction-business,,,전무이사,,,M26029,m26029@hanmaceng.co.kr
,cjpark2@hanmaceng.co.kr,박창종,010-8481-6276,user,construction-business,,,전무이사,,,M24010,m24010@hanmaceng.co.kr
,cspark2@hanmaceng.co.kr,박춘선,010-2551-0986,user,construction-business,,,전무이사,,,M17503,aa200a@hanmaceng.co.kr
,smseo@hanmaceng.co.kr,서상모,010-3712-8479,user,construction-business,,,전무이사,,,M20513,m20513@hanmaceng.co.kr
,shsong2@hanmaceng.co.kr,송세화,010-2619-9140,user,construction-business,,,전무이사,,,M16507,swgamri1@hanmaceng.co.kr
,sioh@hanmaceng.co.kr,오세임,010-9246-4326,user,construction-business,,,전무이사,,,M15406,sioh9064@hanmaceng.co.kr
,gyyu@hanmaceng.co.kr,유광열,010-6891-5010,user,construction-business,,,전무이사,,,M26028,m26028@hanmaceng.co.kr
,ysyu@hanmaceng.co.kr,유영석,010-6362-6253,user,construction-business,,,전무이사,,,M24030,yec655@hanmaceng.co.kr
,jjyu@hanmaceng.co.kr,유정진,010-3830-6237,user,construction-business,,,전무이사,,,M10502,rnwlrnwl@hanmaceng.co.kr
,swyoon@hanmaceng.co.kr,윤성욱,010-2613-3354,user,construction-business,,,전무이사,,,M22049,m22049@hanmaceng.co.kr
,jdyoon@hanmaceng.co.kr,윤종대,010-5092-0797,user,construction-business,,,전무이사,,,M20405,jdyoona@hanmaceng.co.kr
,dslee3@hanmaceng.co.kr,이동식,010-5317-5303,user,construction-business,,,전무이사,,,M23011,m23011@hanmaceng.co.kr
,dhlee7@hanmaceng.co.kr,이동희,010-6334-2365,user,construction-business,,,전무이사,,,M16504,edonghee@hanmaceng.co.kr
,bglee@hanmaceng.co.kr,이병계,010-5484-3822,user,construction-business,,,전무이사,,,M26035,m24078@hanmaceng.co.kr
,srlee2@hanmaceng.co.kr,이상록,010-2694-0159,user,construction-business,,,전무이사,,,M26002,m26002@hanmaceng.co.kr
,sslee2@hanmaceng.co.kr,이상세,010-3417-2807,user,construction-business,,,전무이사,,,M23048,m23048@hanmaceng.co.kr
,swlee5@hanmaceng.co.kr,이상원,010-2260-2729,user,construction-business,,,전무이사,,,M20407,swlee410@hanmaceng.co.kr
,sglee3@hanmaceng.co.kr,이성규,010-7173-7606,user,construction-business,,,전무이사,,,M24024,m24024@hanmaceng.co.kr
,hglee3@hanmaceng.co.kr,이현구A,010-5240-2863,user,construction-business,,,전무이사,,,M25028,odysseylee69@gmail.com
,hiim@hanmaceng.co.kr,임호인,010-9984-8375,user,construction-business,,,전무이사,,,M26008,m26008@hanmaceng.co.kr
,gsjang@hanmaceng.co.kr,장길상,010-5402-8257,user,construction-business,,,전무이사,,,M21452,m21452@hanmaceng.co.kr
,esjeon@hanmaceng.co.kr,전은수,010-3664-5703,user,construction-business,,,전무이사,,,M16401,m16401@hanmaceng.co.kr
,jsjeon@hanmaceng.co.kr,전준수,010-3591-7011,user,construction-business,,,전무이사,,,M16511,junsu0904@hanmaceng.co.kr
,rgjeong@hanmaceng.co.kr,정락경,010-3205-3994,user,construction-business,,,전무이사,,,M26015,m26015@hanmaceng.co.kr
,sbjeong@hanmaceng.co.kr,정상범,010-8907-9883,user,construction-business,,,전무이사,,,M17402,cjgh707@hanmaceng.co.kr
,wcjeong2@hanmaceng.co.kr,정우창,010-6563-0034,user,construction-business,,,전무이사,,,M25004,m25004@hanmaceng.co.kr
,jhjeong3@hanmaceng.co.kr,정종호,010-5500-2532,user,construction-business,,,전무이사,,,M23078,m23078@hanmaceng.co.kr
,swcho2@hanmaceng.co.kr,조수원,010-3725-7539,user,construction-business,,,전무이사,,,M26016,m26016@hanmaceng.co.kr
,jgju@hanmaceng.co.kr,주재강,010-3690-8652,user,construction-business,,,전무이사,,,M16313,joojkangg@hanmaceng.co.kr
,hbjin@hanmaceng.co.kr,진현범,010-4365-2282,user,construction-business,,,전무이사,,,M24070,m24070@hanmaceng.co.kr
,ischae@hanmaceng.co.kr,채일석,010-2650-3232,user,construction-business,,,전무이사,,,M23025,chae32@hanmaceng.co.kr
,gjchoi@hanmaceng.co.kr,최규진,010-3856--5031,user,construction-business,,,전무이사,,,M26034,-
,hschoi3@hanmaceng.co.kr,최희섭,010-2680-2890,user,construction-business,,,전무이사,,,M26011,m26011@hanmaceng.co.kr
,sthan@hanmaceng.co.kr,한상태,010-7275-2709,user,construction-business,,,전무이사,,,M23030,m23030@hanmaceng.co.kr
,chheo@hanmaceng.co.kr,허찬행,010-7207-2339,user,construction-business,,,전무이사,,,M21322,m21322@hanmaceng.co.kr
,dihwang@hanmaceng.co.kr,황대익,010-3069-7430,user,construction-business,,,전무이사,,,J05204,dihwang@hanmaceng.co.kr
,smko@hanmaceng.co.kr,고석만,010-4995-3275,user,construction-business,,,상무이사,,,M22030,m22030@hanmaceng.co.kr
,ghkim7@hanmaceng.co.kr,김경환,010-8589-7560,user,construction-business,,,상무이사,,,M17504,pintree0522@hanmaceng.co.kr
,yskim13@hanmaceng.co.kr,김유식,010-8940-8772,user,construction-business,,,상무이사,,,M26012,m26012@hanmaceng.co.kr
,jykim10@hanmaceng.co.kr,김종용,010-9365-2389,user,construction-business,,,상무이사,,,M25072,m25072@hanmaceng.co.kr
,hgan@hanmaceng.co.kr,안흥권,010-3127-9962,user,construction-business,,,상무이사,,,M26007,m26007@hanmaceng.co.kr
,jsyang@hanmaceng.co.kr,양정선,010-9361-3569,user,construction-business,,,상무이사,,,M20406,yangjs3569@hanmaceng.co.kr
,ygyoon@hanmaceng.co.kr,윤여길,010-5658-9533,user,construction-business,,,상무이사,,,M26009,m26009@hanmaceng.co.kr
,glee2@hanmaceng.co.kr,이규,010-6339-9712,user,construction-business,,,상무이사,,,M26027,m26027@hanmaceng.co.kr
,swlee6@hanmaceng.co.kr,이선욱,010-5286-9782,user,construction-business,,,전무이사,,,M02203,sul96@hanmaceng.co.kr
,wlee@hanmaceng.co.kr,이웅,010-8604-1704,user,construction-business,,,상무이사,,,M24054,m24054@hanmaceng.co.kr
,jkchoi3@hanmaceng.co.kr,최정길,010-2240-9657,user,construction-business,,,상무이사,,,M24064,m24064@hanmaceng.co.kr
,jhchoi11@hanmaceng.co.kr,최종현,010-7416-7406,user,construction-business,,,상무이사,,,M26030,m26030@hanmaceng.co.kr
,yckang@hanmaceng.co.kr,강영철,010-5751-9279,user,construction-business,,,이사,,,M25049,m25049@hanmaceng.co.kr
,hskang@hanmaceng.co.kr,강현승,010-4662-6105,user,construction-business,,,이사,,,M22077,m22077@hanmaceng.co.kr
,dhkim7@hanmaceng.co.kr,김동한,010-6562-1795,user,construction-business,,,이사,,,M25055,m25055@hanmaceng.co.kr
,yskim11@hanmaceng.co.kr,김양석,010-4611-1307,user,construction-business,,,이사,,,M23046,herykim@hanmaceng.co.kr
,yskim12@hanmaceng.co.kr,김윤성,010-5718-0344,user,construction-business,,,이사,,,M24007,rlays80@hanmaceng.co.kr
,jmkim7@hanmaceng.co.kr,김정문,010-9899-3922,user,construction-business,,,이사,,,M23008,m23008@hanmaceng.co.kr
,sgoh@hanmaceng.co.kr,오세걸,010-8750-2827,user,construction-business,,,이사,,,M26032,m26032@hanmaceng.co.kr
,yglee2@hanmaceng.co.kr,이영규,010-3713-0529,user,construction-business,,,이사,,,M23084,m23084@hanmaceng.co.kr
,jslee5@hanmaceng.co.kr,이재성,010-3043-8848,user,construction-business,,,이사,,,M25063,m25063@hanmaceng.co.kr
,jblee@hanmaceng.co.kr,이종범,010-4046-3158,user,construction-business,,,이사,,,M25048,m25048@hanmaceng.co.kr
,jijung@hanmaceng.co.kr,정재익,010-4124-3452,user,construction-business,,,이사,,,M05305,jjaeick@hanmaceng.co.kr
,jhjeong2@hanmaceng.co.kr,정재혁,010-2300-0070,user,construction-business,,,이사,,,M26019,m26019@hanmaceng.co.kr
,ghcho@hanmaceng.co.kr,조규형,010-3357-8020,user,construction-business,,,이사,,,M24072,m23023@hanmaceng.co.kr
,sgchoi2@hanmaceng.co.kr,최성길,010-4332-0642,user,construction-business,,,이사,,,M23031,m23031@hanmaceng.co.kr
,jgchoi2@hanmaceng.co.kr,최재곤,010-9168-5632,user,construction-business,,,이사,,,M25005,m25005@hanmaceng.co.kr
,jwchoi7@hanmaceng.co.kr,최진우,010-6546-1121,user,construction-business,,,이사,,,M24077,civil96@hanmaceng.co.kr
,kskang2@hanmaceng.co.kr,강길성,010-8507-9840,user,construction-business,,,부장,,,M26004,m26004@hanmaceng.co.kr
,jhkim33@hanmaceng.co.kr,김정훈A,010-9437-8183,user,construction-business,,,부장,,,M24067,jh067@hanmaceng.co.kr
,igpark@hanmaceng.co.kr,박인규,010-2125-9748,user,construction-business,,,부장,,,M22016,m21464@hanmaceng.co.kr
,hcyang@hanmaceng.co.kr,양희찬,010-4501-0297,user,construction-business,,,부장,,,M23065,m23065@hanmaceng.co.kr
,jhyu2@hanmaceng.co.kr,유지훈,010-2842-4746,user,construction-business,,,부장,,,M25069,m25069@hanmaceng.co.kr
,dslee4@hanmaceng.co.kr,이동선,010-6369-7952,user,construction-business,,,부장,,,M26020,m26020@hanmaceng.co.kr
,jykoo@hanmaceng.co.kr,구자용,010-3042-1985,user,construction-business,,,과장,,,M20311,jayong23@hanmaceng.co.kr
,nhseo@hanmaceng.co.kr,서남호,010-2361-8585,user,construction-business,,,과장,,,M23035,m23035@hanmaceng.co.kr
,swryu@hanmaceng.co.kr,류시우,010-7554-5728,user,construction-business,,,사원,,,M24057,m24057@hanmaceng.co.kr
,cylee3@hanmaceng.co.kr,이창용,010-9304-4047,user,safety-management,,,전무이사,,,M05508,lcy3392@hanmaceng.co.kr
,mhjeong@hanmaceng.co.kr,정미희,010-5373-4697,user,safety-management,,,대리,,,M22007,kokoball22@hanmaceng.co.kr
도로부,hklee3@hanmaceng.co.kr,이현구,010-8365-3205,user,infrastructure-hq,,,부사장,본부장,,M16222,hmokja@hanmaceng.co.kr
,hslee6@hanmaceng.co.kr,이환섭,010-7763-5463,user,infra-road,,,부사장,부서장,,M22031,m22031@hanmaceng.co.kr
,dhlee8@hanmaceng.co.kr,이동훈,010-7930-2170,user,infra-road,,,전무,,,M03201,suny8561@hanmaceng.co.kr
,yglee3@hanmaceng.co.kr,이영경,010-9016-3227,user,infra-road,,,전무,,,M02213,yglee@hanmaceng.co.kr
,kysong@hanmaceng.co.kr,송기영,010-8641-5769,user,infra-road,,,상무,,,M07231,karet@hanmaceng.co.kr
,kjlee@hanmaceng.co.kr,이기주,010-3347-2661,user,infra-road,,,상무,,,M15207,lgj0906@hanmaceng.co.kr
,cykim2@hanmaceng.co.kr,김정열,010-7239-2807,user,infra-road,,,이사,,,J05215,comrade94@hanmaceng.co.kr
,cskim@hanmaceng.co.kr,김창섭,010-2725-1074,user,infra-road,,,이사,,,M22035,m22035@hanmaceng.co.kr
,yjjeong2@hanmaceng.co.kr,정유종,010-4003-0329,user,infra-road,,,이사,,,M25039,m25039@hanmaceng.co.kr
,jwlee8@hanmaceng.co.kr,이정원,010-3741-0368,user,infra-road,,,이사,,,M15206,with2u@hanmaceng.co.kr
,csjeon@hanmaceng.co.kr,전찬성,010-6267-3181,user,infra-road,,,이사,,,M07317,bravo635@hanmaceng.co.kr
,kcpark@hanmaceng.co.kr,박기철,010-3572-7073,user,infra-road,,,이사,,,M25020,m25020@hanmaceng.co.kr
,mhahn@hanmaceng.co.kr,안민형,010-3559-3246,user,infra-road,,,부장,,,M22003,m22003@hanmaceng.co.kr
,twkim@hanmaceng.co.kr,김태우,010-7317-8668,user,infra-road,,,과장,,,M20327,m20327@hanmaceng.co.kr
,wylee@hanmaceng.co.kr,이원용,010-8562-7199,user,infra-road,,,과장,,,M21460,m21460@hanmaceng.co.kr
,cnamgung@hanmaceng.co.kr,남궁찬,010-9973-7314,user,infra-road,,,과장,,,M22032,m22032@hanmaceng.co.kr
,hoseo2@hanmaceng.co.kr,서현옥,010-6314-7382,user,infra-road,,,과장,,,B15203,hyunok.seo@hanmaceng.co.kr
,jwkim12@hanmaceng.co.kr,김종우,010-5924-2083,user,infra-road,,,과장,,,M24082,m24082@hanmaceng.co.kr
,mspark3@hanmaceng.co.kr,박민수,010-4056-0530,user,infra-road,,,과장,,,M20305,msms1993@hanmaceng.co.kr
,jhjin@hanmaceng.co.kr,진장현,010-7212-9015,user,infra-road,,,과장,,,M23064,m23064@hanmaceng.co.kr
,sikwon@hanmaceng.co.kr,권순일,010-8855-1279,user,infra-road,,,대리,,,M21303,m21303@hanmaceng.co.kr
,hjjeong2@hanmaceng.co.kr,정혜진,010-3706-1119,user,infra-road,,,대리,,,M16317,hyejin@hanmaceng.co.kr
,sbchoi2@hanmaceng.co.kr,최승범,010-2504-3509,user,infra-road,,,대리,,,M21432,m21432@hanmaceng.co.kr
,hjkim12@hanmaceng.co.kr,김효진,010-5510-3281,user,infra-road,,,대리,,,M25038,m24020@hanmaceng.co.kr
,ghlee6@hanmaceng.co.kr,이교호,010-2291-1996,user,infra-road,,,사원,,,M23005,m23005@hanmaceng.co.kr
,jhsong2@hanmaceng.co.kr,송주호,010-6691-5131,user,infra-road,,,사원,,,M25007,m25007@hanmaceng.co.kr
,jhju@hanmaceng.co.kr,주지한 ,010-9658-1289,user,infra-road,,,사원,,,M25006,m25006@hanmaceng.co.kr
,wkkim2@hanmaceng.co.kr,김웅기,010-6347-0667,user,sales-support,,,부사장,,,M24050,wkikim@hanmaceng.co.kr
임원실,scjang@hanmaceng.co.kr,장석춘,010-9398-6896,user,sales-support,,,부사장,,,M19101,jangchun18@naver.com
,dskim4@hanmaceng.co.kr,김동수,010-5368-8504,user,sales-support,,,사장,,,M25066,m25066@hanmaceng.co.kr
,hjjang2@hanmaceng.co.kr,장혁진,010-8284-5434,user,sales-support,,,부장,,,M25001,m25001@hanmaceng.co.kr
,jdpark@hanmaceng.co.kr,박준동,010-6663-2312,user,sales-support,,,부사장,,,M25074,m25074@hanmaceng.co.kr
,bgahn@hanmaceng.co.kr,안병구,010-6353-7725,user,sales-support,,,부장,,,M24071,m24071@hanmaceng.co.kr
,nckim@hanmaceng.co.kr,김남철,010-8004-1523,user,sales-support,,,부사장,,,M24004,m24004@hanmaceng.co.kr
,thlee@hanmaceng.co.kr,이태환,010-5372-1458,user,sales-support,,,부사장,,,M24053,ldy1474226@naver.com
,djkim2@hanmaceng.co.kr,김대진,010-4820-0271,user,sales-support,,,사장,,,M19104,dj279531@hanmaceng.co.kr
,dhhan3@hanmaceng.co.kr,한동호,010-3841-1036,user,sales-support,,,부사장,,,M25073,m25073@hanmaceng.co.kr
,sclee3@hanmaceng.co.kr,이승철,010-3260-2533,user,sales-support,,,부사장,,,M24008,m24008@hanmaceng.co.kr
,shoh2@hanmaceng.co.kr,오세현,010-6505-8767,user,sales-support,,,부사장,,,M24049,m24049@hanmaceng.co.kr
,bjpark@hanmaceng.co.kr,박범진,010-7118-6193,user,sales-support,,,부사장,,,M23002,bjpark9361@naver.com
,mhseol@hanmaceng.co.kr,설문형,010-9901-0490,user,sales-support,,,부사장,,,M24014,m24014@hanmaceng.co.kr
,ockwon@hanmaceng.co.kr,권오춘,010-5382-3777,user,sales-support,,,부사장,,,M21436,m21436@hanmaceng.co.kr
,knlee@hanmaceng.co.kr,이규남,010-8568-2144,user,sales-support,,,부사장,,,M21102,m21102@hanmaceng.co.kr
환경,sskim7@hanmaceng.co.kr,김성수,010-8745-3105,user,land-env-assessment,,,부사장,,,M12204,m12204@hanmaceng.co.kr
,sjkang3@hanmaceng.co.kr,강선준,010-4056-6885,user,land-env-assessment,,,부사장,,,M09104,sjkang1226@hanmaceng.co.kr
,bdlee@hanmaceng.co.kr,이병도,010-5166-9329,user,land-env-assessment,,,전무,부서장,,M20101,bottleroad@hanmaceng.co.kr
,cwlee2@hanmaceng.co.kr,이찬우,010-6403-0242,user,land-env-assessment,,,전무,,,M05207,lchanw@hanmaceng.co.kr
,jwlee7@hanmaceng.co.kr,이정우,010-9408-9042,user,land-env-assessment,,,상무,,,M23071,m23071@hanmaceng.co.kr
,bwlee2@hanmaceng.co.kr,이병욱,010-2406-6787,user,land-env-assessment,,,이사,,,M16216,lbw002@hanmaceng.co.kr
,djlee2@hanmaceng.co.kr,이동준,010-4447-1980,user,land-env-assessment,,,이사,,,M20205,djlee7164@hanmaceng.co.kr
,sikim@hanmaceng.co.kr,김승일,010-2769-2104,user,land-env-assessment,,,이사,,,M21434,ryan23@hanmaceng.co.kr
,bcseo@hanmaceng.co.kr,서병철,010-3760-2579,user,land-env-assessment,,,부장,,,M12207,mintwiz@hanmaceng.co.kr
,yhkim@hanmaceng.co.kr,김용현,010-7156-1174,user,land-env-assessment,,,차장,,,M18205,kimyh1174@hanmaceng.co.kr
,ghlee5@hanmaceng.co.kr,이가형,010-6471-6300,user,land-env-assessment,,,차장,,,M22001,m22001@hanmaceng.co.kr
,dyjung2@hanmaceng.co.kr,정도영,010-4882-5145,user,land-env-assessment,,,차장,,,M22074,jdy5145@hanmaceng.co.kr
,hjcho2@hanmaceng.co.kr,조현준,010-4797-6247,user,land-env-assessment,,,과장,,,M23022,m23022@hanmaceng.co.kr
,mspark2@hanmaceng.co.kr,박민상,010-4389-8666,user,land-env-assessment,,,과장,,,M18308,minsang404@hanmaceng.co.kr
,smhong@hanmaceng.co.kr,홍세민,010-5231-6026,user,land-env-assessment,,,과장,,,M20208,tpals32@hanmaceng.co.kr
,yskim10@hanmaceng.co.kr,김용석,010-9991-6082,user,land-env-assessment,,,과장,,,M19329,kyss6082@hanmaceng.co.kr
,hjpark2@hanmaceng.co.kr,박호진,010-9257-8917,user,land-env-assessment,,,대리,,,M22064,m22064@hanmaceng.co.kr
,hglee2@hanmaceng.co.kr,이현건,010-2994-4516,user,land-env-assessment,,,대리,,,M22067,m22067@hanmaceng.co.kr
,sjyu@hanmaceng.co.kr,유수지,010-4191-2018,user,land-env-assessment,,,대리,,,M22056,m22056@hanmaceng.co.kr
,jkchoi2@hanmaceng.co.kr,최진경,010-7412-3248,user,land-env-assessment,,,사원,,,M24043,m24043@hanmaceng.co.kr
1 email name phone role tenant_slug department grade position jobTitle employee_id sub_email
2 교통부 sskim6@hanmaceng.co.kr 김상수 010-5216-9660 user traffic 전무 부서장 M12203 sskim@hanmaceng.co.kr
3 yjkim8@hanmaceng.co.kr 김용종 010-6721-0814 user traffic 상무 M21445 topyj@hanmaceng.co.kr
4 kkimyk@hanmaceng.co.kr 김영권 010-6405-1797 user traffic 상무 M02259 kkimyk@hanmaceng.co.kr
5 yheo@hanmaceng.co.kr 허윤 010-2569-1504 user traffic 과장 M25042 m25042@hanmaceng.co.kr
6 jhkim12@hanmaceng.co.kr 김준형 010-2623-9508 user traffic 대리 M21435 m21435@hanmaceng.co.kr
7 siyoon@hanmaceng.co.kr 윤선일 010-4353-7600 user traffic 대리 M23010 m23010@hanmaceng.co.kr
8 nhlee@hanmaceng.co.kr 이난희 010-4398-3410 user traffic 사원 M25044 m25044@hanmaceng.co.kr
9 shmun@hanmaceng.co.kr 문수하 010-5648-0314 user traffic 사원 M25068 m25068@hanmaceng.co.kr
10 구조부 ykshin2@hanmaceng.co.kr 신영각 010-3290-6057 user infra-structures 부사장 M16208 twindol@hanmaceng.co.kr
11 shyoon@hanmaceng.co.kr 윤성호 010-5286-8476 user infra-structures 부사장 부서장 M10103 feel365@hanmaceng.co.kr
12 jboh@hanmaceng.co.kr 오재범 010-9173-3560 user infra-structures 전무 M03202 jbeom@hanmaceng.co.kr
13 jykim9@hanmaceng.co.kr 김재용 010-9627-8774 user infra-structures 상무 M03228 kjy730@hanmaceng.co.kr
14 shhwang@hanmaceng.co.kr 황승현 010-8727-8306 user infra-structures 상무 M02308 hshksy7367@hanmaceng.co.kr
15 shlee23@hanmaceng.co.kr 이세화 010-9479-2199 user infra-structures 부장 M18204 shlee81@hanmaceng.co.kr
16 dskim5@hanmaceng.co.kr 김대성 010-8709-6322 user infra-structures 차장 M24016 m24016@hanmaceng.co.kr
17 sdkim4@hanmaceng.co.kr 김성도 010-5191-2520 user infra-structures 과장 M21439 m21439@hanmaceng.co.kr
18 jmsong@hanmaceng.co.kr 송재명 010-2393-5797 user infra-structures 과장 M24017 m24017@hanmaceng.co.kr
19 ohjeon@hanmaceng.co.kr 전오현 010-2464-6231 user infra-structures 과장 M25067 m25067@hanmaceng.co.kr
20 ycchoi@hanmaceng.co.kr 최영철 010-9746-4146 user infra-structures 과장 M25018 m25018@hanmaceng.co.kr
21 jmhwang2@hanmaceng.co.kr 황지만 010-2235-5634 user infra-structures 과장 M23042 m23042@hanmaceng.co.kr
22 twkim2@hanmaceng.co.kr 김태욱 010-6577-1853 user infra-structures 대리 M23006 m23006@hanmaceng.co.kr
23 jkban@hanmaceng.co.kr 반진관 010-2677-4083 user infra-structures 대리 M21407 m21407@hanmaceng.co.kr
24 mjoh@hanmaceng.co.kr 오민종 010-8258-3400 user infra-structures 대리 M23054 m23054@hanmaceng.co.kr
25 mkjeon@hanmaceng.co.kr 전민기 010-9945-0295 user infra-structures 대리 M24011 m24011@hanmaceng.co.kr
26 jmson@hanmaceng.co.kr 손지민 010-5195-8257 user infra-structures 사원 M22055 m22055@hanmaceng.co.kr
27 wsshim@hanmaceng.co.kr 심우석 010-7548-7885 user infra-structures 사원 M25017 m25017@hanmaceng.co.kr
28 지반 hmlee3@hanmaceng.co.kr 이한민 010-3255-9884 user infra-geotech-tunnel 상무 부서장 M20105 fastmin@hanmaceng.co.kr
29 jilee2@hanmaceng.co.kr 이종익 010-3869-3451 user infra-geotech-tunnel 상무 M07223 jongiklee@hanmaceng.co.kr
30 sbsim@hanmaceng.co.kr 심성보 010-7242-2931 user infra-geotech-tunnel 이사 M24036 m24036@hanmaceng.co.kr
31 jkyu@hanmaceng.co.kr 유재극 010-8227-0078 user infra-geotech-tunnel 이사 M20211 visyase@hanmaceng.co.kr
32 tskim2@hanmaceng.co.kr 김태식 010-9822-8592 user infra-geotech-tunnel 차장 M21451 m21451@hanmaceng.co.kr
33 mskang2@hanmaceng.co.kr 강민수 010-7933-0183 user infra-geotech-tunnel 과장 M25021 m25021@hanmaceng.co.kr
34 sbpark2@hanmaceng.co.kr 박상빈 010-8281-7928 user infra-geotech-tunnel 과장 M23076 m23076@hanmaceng.co.kr
35 mjsong2@hanmaceng.co.kr 송민제 010-7268-3500 user infra-geotech-tunnel 과장 M24075 m22022@hanmaceng.co.kr
36 swpark4@hanmaceng.co.kr 박선우 010-4823-7458 user infra-geotech-tunnel 대리 M20321 m20321@hanmaceng.co.kr
37 sjjeon@hanmaceng.co.kr 전수진 010-3791-6816 user infra-geotech-tunnel 대리 M21468 m21468@hanmaceng.co.kr
38 jtkim2@hanmaceng.co.kr 김진토 010-9370-4673 user infra-geotech-tunnel 사원 M26014 m26014@hanmaceng.co.kr
39 도시 cgkim@hanmaceng.co.kr 김춘근 010-3354-8398 user land-env-urban-planning 전무 M21413 m21413@hanmaceng.co.kr
40 hyjeong2@hanmaceng.co.kr 정혜연 010-2391-2158 user land-env-urban-planning 전무 부서장 M14101 myisland@hanmaceng.co.kr
41 hhshin@hanmaceng.co.kr 신현호 010-9316-2217 user land-env-urban-planning 상무 M21458 m21458@hanmaceng.co.kr
42 dynam@hanmaceng.co.kr 남동윤 010-5416-1408 user land-env-urban-planning 이사 M21472 m21472@hanmaceng.co.kr
43 hkchoi2@hanmaceng.co.kr 최현근 010-6205-2355 user land-env-urban-planning 이사 M24045 m24045@hanmaceng.co.kr
44 gwkim2@hanmaceng.co.kr 김건우 010-9324-5023 user land-env-urban-planning 부장 M14301 kgenu@hanmaceng.co.kr
45 jwshin3@hanmaceng.co.kr 신진우 010-3434-2949 user land-env-urban-planning 부장 M21414 m21414@hanmaceng.co.kr
46 skryu@hanmaceng.co.kr 류성균 010-4211-5202 user land-env-urban-planning 부장 M25022 m25022@hanmaceng.co.kr
47 jkhwang@hanmaceng.co.kr 황재광 010-4780-0137 user land-env-urban-planning 차장 M25050 m25050@hanmaceng.co.kr
48 jwbae2@hanmaceng.co.kr 배진우 010-7501-1272 user land-env-urban-planning 과장 M24073 m24073@hanmaceng.co.kr
49 yjlee7@hanmaceng.co.kr 이예지 010-9937-1276 user land-env-urban-planning 과장 M21462 m21462@hanmaceng.co.kr
50 dhchoi@hanmaceng.co.kr 최대헌 010-2214-9153 user land-env-urban-planning 과장 M24038 m24038@hanmaceng.co.kr
51 ghkim@hanmaceng.co.kr 김관후 010-6302-3157 user land-env-urban-planning 대리 M22051 m22051@hanmaceng.co.kr
52 mssong@hanmaceng.co.kr 송미성 010-6258-4972 user land-env-urban-planning 대리 M25027 m25027@hanmaceng.co.kr
53 shshin@hanmaceng.co.kr 신송희 010-9518-4988 user land-env-urban-planning 대리 M21422 m21422@hanmaceng.co.kr
54 hjchae@hanmaceng.co.kr 채호주 010-9038-8389 user land-env-urban-planning 대리 M23070 m23070@hanmaceng.co.kr
55 wskim4@hanmaceng.co.kr 김원섭 010-9117-7449 user land-env-urban-planning 사원 M24062 m24062@hanmaceng.co.kr
56 jeeim@hanmaceng.co.kr 임지은 010-3994-3634 user land-env-urban-planning 사원 M25047 m25047@hanmaceng.co.kr
57 수자 HGLEE4@hanmaceng.co.kr 이홍규 010-6201-6542 user land-env-water-resources 부사장 M19103 hglee@hanmaceng.co.kr
58 JCHYUN@hanmaceng.co.kr 현종철 010-6204-3628 user land-env-water-resources 전무 부서장 M21410 m21410@hanmaceng.co.kr
59 JJLEE4@hanmaceng.co.kr 이재진 010-2470-7923 user land-env-water-resources 이사 M08206 ljj7924@hanmaceng.co.kr
60 BHLEE@hanmaceng.co.kr 이병화 010-9133-5068 user land-env-water-resources 이사 M21428 m21428@hanmaceng.co.kr
61 SCSEO@hanmaceng.co.kr 서순창 010-9899-8042 user land-env-water-resources 부장 M21425 m21425@hanmaceng.co.kr
62 JBYOO@hanmaceng.co.kr 유재범 010-7100-7942 user land-env-water-resources 차장 M16308 jboom@hanmaceng.co.kr
63 DYKIM@hanmaceng.co.kr 김덕용 010-6235-5976 user land-env-water-resources 차장 M22058 m22058@hanmaceng.co.kr
64 SHHYUN@hanmaceng.co.kr 현석훈 010-9932-0236 user land-env-water-resources 차장 M21454 m21454@hanmaceng.co.kr
65 YHHWANG2@hanmaceng.co.kr 황윤희 010-8484-8721 user land-env-water-resources 과장 M24025 m24025@hanmaceng.co.kr
66 SHJUNG@hanmaceng.co.kr 정승훈 010-5192-8290 user land-env-water-resources 대리 M25012 m25012@hanmaceng.co.kr
67 MSKIM13@hanmaceng.co.kr 김명식 010-2538-4933 user land-env-water-resources 대리 M22008 myungsik@hanmaceng.co.kr
68 DBLEE@hanmaceng.co.kr 이다빈 010-9738-1325 user land-env-water-resources 대리 M25011 m25011@hanmaceng.co.kr
69 WJCHO@hanmaceng.co.kr 조우진 010-3327-0817 user land-env-water-resources 대리 M23058 m23058@hanmaceng.co.kr
70 상하 sspark3@hanmaceng.co.kr 박승신 010-2272-1436 user water-sewerage 부사장 부서장 M22009 m22009@hanmaceng.co.kr
71 dwlee4@hanmaceng.co.kr 이동욱 010-6339-1959 user water-sewerage 전무이사 M20104 quaaahoo@hanmaceng.co.kr
72 ywgil@hanmaceng.co.kr 길이원 010-9917-0157 user water-sewerage 차장 M20217 m20217@hanmaceng.co.kr
73 dwkim5@hanmaceng.co.kr 김동욱 010-7705-7489 user water-sewerage 과장 M26010 m26010@hanmaceng.co.kr
74 sjkim10@hanmaceng.co.kr 김성주 010-3885-8068 user water-sewerage 과장 M25058 m25058@hanmaceng.co.kr
75 yjim@hanmaceng.co.kr 임연재 010-4223-3881 user water-sewerage 대리 M22066 m22066@hanmaceng.co.kr
76 bkkim@hanmaceng.co.kr 김병국 010-9303-3245 user water-sewerage 대리 M26003 m26003@hanmaceng.co.kr
77 yhlim@hanmaceng.co.kr 임유화 010-8383-3522 user water-sewerage 사원 M23038 m23038@hanmaceng.co.kr
78 ybsim@hanmaceng.co.kr 심윤보 010-5125-3016 user water-sewerage 사원 M24065 m24065@hanmaceng.co.kr
79 dhhwang2@hanmaceng.co.kr 황동훈 010-2505-1873 user water-sewerage 사원 M25043 m25043@hanmaceng.co.kr
80 안전 sako@hanmaceng.co.kr 고삼암 010-5361-7995 user safety-diagnosis 부회장 M04503 ksam51@hanmaceng.co.kr
81 hsbyoun@hanmaceng.co.kr 변한석 010-6641-9995 user safety-diagnosis 전무 부서장 M22057 m22057@hanmaceng.co.kr
82 kjlee2@hanmaceng.co.kr 이기종 010-3899-9830 user safety-diagnosis 전무 M06505 lkj0997@hanmaceng.co.kr
83 jsjo@hanmaceng.co.kr 조증식 010-5350-6952 user safety-diagnosis 전무 M23081 m23081@hanmaceng.co.kr
84 hclim@hanmaceng.co.kr 임현철 010-8749-9448 user safety-diagnosis 상무 M21455 m21455@hanmaceng.co.kr
85 jyson2@hanmaceng.co.kr 손재용 010-9009-8961 user safety-diagnosis 이사 M08306 son0802@hanmaceng.co.kr
86 hmyeon@hanmaceng.co.kr 연훈모 010-7923-8703 user safety-diagnosis 차장 M24029 yhm8703@hanmaceng.co.kr
87 sgjeon2@hanmaceng.co.kr 전성구 010-9199-8060 user safety-diagnosis 차장 M21429 m21429@hanmaceng.co.kr
88 ejkim@hanmaceng.co.kr 김응진 010-9960-1522 user safety-diagnosis 과장 M21457 m21457@hanmaceng.co.kr
89 tykim3@hanmaceng.co.kr 김태연 010-7171-5261 user safety-diagnosis 과장 M20306 ququ44@hanmaceng.co.kr
90 wjna@hanmaceng.co.kr 나원준 010-6652-8978 user safety-diagnosis 과장 M25014 m25014@hanmaceng.co.kr
91 gspark3@hanmaceng.co.kr 박건석 010-6225-0356 user safety-diagnosis 과장 M25015 m25015@hanmaceng.co.kr
92 ghpark4@hanmaceng.co.kr 박건희 010-6434-7402 user safety-diagnosis 과장 M25016 m25016@hanmaceng.co.kr
93 ghbeak@hanmaceng.co.kr 백건휘 010-8762-9098 user safety-diagnosis 과장 M23001 m23001@hanmaceng.co.kr
94 hcyun@hanmaceng.co.kr 윤현철 010-9604-4569 user safety-diagnosis 과장 M23017 m23017@hanmaceng.co.kr
95 jmlee3@hanmaceng.co.kr 이지명 010-4212-0867 user safety-diagnosis 과장 M23014 m23014@hanmaceng.co.kr
96 dvkim@hanmaceng.co.kr 김동빈 010-9417-1161 user safety-diagnosis 대리 M25030 m25030@hanmaceng.co.kr
97 tykim2@hanmaceng.co.kr 김태윤 010-2866-1853 user safety-diagnosis 대리 M20312 xotls10@hanmaceng.co.kr
98 cgyu@hanmaceng.co.kr 유찬근 010-2908-1628 user safety-diagnosis 대리 M25059 m25059@hanmaceng.co.kr
99 mjlee2@hanmaceng.co.kr 이명진 010-7577-6386 user safety-diagnosis 대리 M24023 m24023@hanmaceng.co.kr
100 whjeong@hanmaceng.co.kr 정원호 010-8470-3996 user safety-diagnosis 대리 M25029 m25029@hanmaceng.co.kr
101 jhkim34@hanmaceng.co.kr 김진호 010-4300-2625 user safety-diagnosis 사원 M24022 m24022@hanmaceng.co.kr
102 hopark@hanmaceng.co.kr 박현옥 010-4002-5162 user safety-diagnosis 사원 M24084 m24084@hanmaceng.co.kr
103 jsyu@hanmaceng.co.kr 유지성 010-9821-1155 user safety-diagnosis 사원 M23037 m23037@hanmaceng.co.kr
104 dhhan2@hanmaceng.co.kr 한동화 010-2054-4899 user safety-diagnosis 과장 M25031 gksehdghk123@naver.com
105 ychong@hanmaceng.co.kr 홍영철 010-9732-5097 user safety-diagnosis 전무 M25032 hycoso@hanmail.net
106 임원실 khlee5@hanmaceng.co.kr 이경훈 010-3733-1890 user hanmac 사장 대표이사 M02107 khoonri@hanmaceng.co.kr
107 경영지원 sryou@hanmaceng.co.kr 유승열 010-5330-6503 user sales-support 부사장 M10102 srryu65@hanmaceng.co.kr
108 hwshin2@brsw.kr 신현우 010-8979-5720 user baroncs 전무 부서장 T02303 hwshin@hanmaceng.co.kr
109 jhkim35@brsw.kr 김재헌 010-8785-2798 user baroncs 상무 J08206 rurouno@hanmaceng.co.kr
110 yioh@hanmaceng.co.kr 오윤익 010-4033-3566 user 경영지원부(SLUG값 없음) 차장 M23040 m23040@hanmaceng.co.kr
111 hmchae@hanmaceng.co.kr 채희문 010-9213-7842 user 경영지원부(SLUG값 없음) 차장 M21321 m21321@hanmaceng.co.kr
112 aylee@hanmaceng.co.kr 이애연 010-3356-0412 user 경영지원부(SLUG값 없음) 과장 M25033 dodus1@hanmaceng.co.kr
113 jwkim10@brsw.kr 김지우 010-4265-5624 user baroncs 과장 T02328 jeewoo@hanmaceng.co.kr
114 jhlee21@brsw.kr 이지혜 010-9528-0710 user baroncs 과장 J14306 leejh@hanmaceng.co.kr
115 jwyu@brsw.kr 유재욱 010-2819-1746 user baroncs 과장 B20334 b20334@hanmaceng.co.kr
116 jhlee22@brsw.kr 이준희 010-3200-6180 user baroncs 과장 B18302 b18302@hanmaceng.co.kr
117 hskim14@hanmaceng.co.kr 김현수 010-5020-4890 user 경영지원부(SLUG값 없음) 과장 M22033 m22033@hanmaceng.co.kr
118 dghyeon@hanmaceng.co.kr 현동규 010-4023-7476 user 경영지원부(SLUG값 없음) 과장 B20305 hdg0310@hanmaceng.co.kr
119 cwkim@hanmaceng.co.kr 김찬웅 010-6485-6536 user 경영지원부(SLUG값 없음) 과장 M23007 ktts1234@hanmaceng.co.kr
120 wsseo@hanmaceng.co.kr 서우석 010-8698-0757 user 경영지원부(SLUG값 없음) 대리 M24046 m24046@hanmaceng.co.kr
121 yjyoon@brsw.kr 윤영주 010-2802-4130 user baroncs 대리 B14303 hanmaceml@hanmaceng.co.kr
122 ymyu@hanmaceng.co.kr 유유민 010-3697-7320 user 경영지원부(SLUG값 없음) 대리 M24013 m24013@hanmaceng.co.kr
123 tsnoh@hanmaceng.co.kr 노태수 010-4819-9179 user 경영지원부(SLUG값 없음) 대리 M22028 b21363@hanmaceng.co.kr
124 yjkim26@hanmaceng.co.kr 김윤지 010-3299-8825 user 경영지원부(SLUG값 없음) 대리 M22046 b21361@hanmaceng.co.kr
125 egkim@hanmaceng.co.kr 김유진 010-8494-1087 user 경영지원부(SLUG값 없음) 사원 M24069 m24069@hanmaceng.co.kr
126 dhahn@hanmaceng.co.kr 안동현 010-4599-4397 user 경영지원부(SLUG값 없음) 사원 M24040 m24040@hanmaceng.co.kr
127 scyoon@brsw.kr 윤성찬 010-2873-3557 user baroncs 사원 B21321 b21321@hanmaceng.co.kr
128 hsoh@brsw.kr 오혜성 010-4067-9435 user baroncs 사원 B21303 b21303@hanmaceng.co.kr
129 jkchung@hanmaceng.co.kr 정준규 010-5346-6479 user 경영지원부(SLUG값 없음) 사원 B25024 b25024@hanmaceng.co.kr
130 wwkang@hanmaceng.co.kr 강우원 010-2317-0910 user construction-business 부사장 M26021 m26021@hanmaceng.co.kr
131 esko@hanmaceng.co.kr 고은식 010-3461-1654 user construction-business 부사장 M25019 m25019@hanmaceng.co.kr
132 jgkim1@hanmaceng.co.kr 김종계 010-3328-2408 user construction-business 부사장 M24032 m24032@hanmaceng.co.kr
133 ispark2@hanmaceng.co.kr 박익수 010-4531-8079 user construction-business 부사장 M24041 m24041@hanmaceng.co.kr
134 tkpark@hanmaceng.co.kr 박택규 010-7588-8558 user construction-business 부사장 M21444 m21444@hanmaceng.co.kr
135 ksshin@hanmaceng.co.kr 신광수 010-5228-4684 user construction-management-h 부사장 본부장 M04602 sks-1211@hanmaceng.co.kr
136 hsshin2@hanmaceng.co.kr 신흥섭 010-3719-1271 user construction-business 부사장 M23029 shs206066@hanmaceng.co.kr
137 ksoh@hanmaceng.co.kr 오관식 010-9769-1005 user construction-business 부사장 M23027 m23027@hanmaceng.co.kr
138 bioh@hanmaceng.co.kr 오방일 010-2217-4907 user construction-business 부사장 M24006 m24006@hanmaceng.co.kr
139 islee2@hanmaceng.co.kr 이인상 010-4584-8504 user construction-business 부사장 M21403 m21403@hanmaceng.co.kr
140 smjeon@hanmaceng.co.kr 전성모 010-7422-2371 user construction-business 부사장 M24058 m24058@hanmaceng.co.kr
141 csham@hanmaceng.co.kr 함창수 010-2837-0372 user construction-business 부사장 M23085 m23085@hanmaceng.co.kr
142 mhkang@hanmaceng.co.kr 강명호 010-2019-2252 user construction-business 전무이사 M16514 kmh6601@hanmaceng.co.kr
143 mhkang2@hanmaceng.co.kr 강민호 010-3415-5033 user construction-business 전무이사 M26031 m26031@hanmaceng.co.kr
144 gwgo@hanmaceng.co.kr 고광욱 010-5358-7394 user construction-business 전무이사 M06501 kokw6489@hanmaceng.co.kr
145 ydkoh@hanmaceng.co.kr 고영덕 010-5488-7809 user construction-business 전무이사 M24056 m24056@hanmaceng.co.kr
146 thkwon1@hanmaceng.co.kr 권태훈 010-7266-8933 user construction-business 전무이사 M22036 m22036@hanmaceng.co.kr
147 ghkim6@hanmaceng.co.kr 김기환 010-9363-8459 user construction-business 전무이사 M23059 m23059@hanmaceng.co.kr
148 djkim3@hanmaceng.co.kr 김동주 010-2630-9999 user construction-business 전무이사 M02608 dhkdj@hanmaceng.co.kr
149 djkim4@hanmaceng.co.kr 김동준 010-4459-3231 user construction-business 전무이사 M21405 m21405@hanmaceng.co.kr
150 bckim2@hanmaceng.co.kr 김병찬 010-8532-2050 user construction-business 전무이사 M15408 chany66@hanmaceng.co.kr
151 bykim@hanmaceng.co.kr 김봉용 010-8524-9039 user construction-business 전무이사 M21401 m21401@hanmaceng.co.kr
152 sdkim5@hanmaceng.co.kr 김석동 010-5388-6687 user construction-business 전무이사 M26033 m25023@hanmaceng.co.kr
153 ygkim3@hanmaceng.co.kr 김양국 010-8545-3191 user construction-business 전무이사 M16503 js030107@hanmaceng.co.kr
154 yhkim2@hanmaceng.co.kr 김용혁 010-3225-6043 user construction-business 전무이사 M21402 m21402@hanmaceng.co.kr
155 wjkim2@hanmaceng.co.kr 김우진 010-3679-5287 user construction-business 전무이사 M24039 kwj3431@hanmaceng.co.kr
156 jhkim36@hanmaceng.co.kr 김재흥 010-8865-5830 user construction-business 전무이사 M25071 m25071@hanmaceng.co.kr
157 jgkim2@hanmaceng.co.kr 김정기 010-5765-3722 user construction-business 전무이사 M23053 m23053@hanmaceng.co.kr
158 chkim4@hanmaceng.co.kr 김찬호 010-5477-3482 user construction-business 전무이사 M23052 m23052@hanmaceng.co.kr
159 hjkim13@hanmaceng.co.kr 김형주 010-6809-2222 user construction-business 전무이사 M24052 m24052@hanmaceng.co.kr
160 htkim@hanmaceng.co.kr 김홍태 010-4544-6403 user construction-business 전무이사 M16513 oklt0720@hanmaceng.co.kr
161 ggnoh@hanmaceng.co.kr 노경국 010-8865-2211 user construction-business 전무이사 M07313 rhodr@hanmaceng.co.kr
162 diryu@hanmaceng.co.kr 류동일 010-2977-6245 user construction-business 전무이사 M24076 m24076@hanmaceng.co.kr
163 bymoon@hanmaceng.co.kr 문백용 010-3768-7918 user construction-business 전무이사 M02606 alangback@hanmaceng.co.kr
164 ihmin@hanmaceng.co.kr 민인홍 010-8752-0940 user construction-business 전무이사 M24034 m24034@hanmaceng.co.kr
165 ghpark@hanmaceng.co.kr 박광환 010-6553-4504 user construction-business 전무이사 M26005 m26005@hanmaceng.co.kr
166 mjpark@hanmaceng.co.kr 박명주 010-3702-0781 user construction-business 전무이사 M25034 m25034@hanmaceng.co.kr
167 mhpark2@hanmaceng.co.kr 박명희 010-9500-6598 user construction-business 전무이사 M26006 m26006@hanmaceng.co.kr
168 yspark5@hanmaceng.co.kr 박연수 010-3671-7768 user construction-business 전무이사 M07507 pys3804@hanmaceng.co.kr
169 jspark10@hanmaceng.co.kr 박종시 010-5375-7388 user construction-business 전무이사 M26029 m26029@hanmaceng.co.kr
170 cjpark2@hanmaceng.co.kr 박창종 010-8481-6276 user construction-business 전무이사 M24010 m24010@hanmaceng.co.kr
171 cspark2@hanmaceng.co.kr 박춘선 010-2551-0986 user construction-business 전무이사 M17503 aa200a@hanmaceng.co.kr
172 smseo@hanmaceng.co.kr 서상모 010-3712-8479 user construction-business 전무이사 M20513 m20513@hanmaceng.co.kr
173 shsong2@hanmaceng.co.kr 송세화 010-2619-9140 user construction-business 전무이사 M16507 swgamri1@hanmaceng.co.kr
174 sioh@hanmaceng.co.kr 오세임 010-9246-4326 user construction-business 전무이사 M15406 sioh9064@hanmaceng.co.kr
175 gyyu@hanmaceng.co.kr 유광열 010-6891-5010 user construction-business 전무이사 M26028 m26028@hanmaceng.co.kr
176 ysyu@hanmaceng.co.kr 유영석 010-6362-6253 user construction-business 전무이사 M24030 yec655@hanmaceng.co.kr
177 jjyu@hanmaceng.co.kr 유정진 010-3830-6237 user construction-business 전무이사 M10502 rnwlrnwl@hanmaceng.co.kr
178 swyoon@hanmaceng.co.kr 윤성욱 010-2613-3354 user construction-business 전무이사 M22049 m22049@hanmaceng.co.kr
179 jdyoon@hanmaceng.co.kr 윤종대 010-5092-0797 user construction-business 전무이사 M20405 jdyoona@hanmaceng.co.kr
180 dslee3@hanmaceng.co.kr 이동식 010-5317-5303 user construction-business 전무이사 M23011 m23011@hanmaceng.co.kr
181 dhlee7@hanmaceng.co.kr 이동희 010-6334-2365 user construction-business 전무이사 M16504 edonghee@hanmaceng.co.kr
182 bglee@hanmaceng.co.kr 이병계 010-5484-3822 user construction-business 전무이사 M26035 m24078@hanmaceng.co.kr
183 srlee2@hanmaceng.co.kr 이상록 010-2694-0159 user construction-business 전무이사 M26002 m26002@hanmaceng.co.kr
184 sslee2@hanmaceng.co.kr 이상세 010-3417-2807 user construction-business 전무이사 M23048 m23048@hanmaceng.co.kr
185 swlee5@hanmaceng.co.kr 이상원 010-2260-2729 user construction-business 전무이사 M20407 swlee410@hanmaceng.co.kr
186 sglee3@hanmaceng.co.kr 이성규 010-7173-7606 user construction-business 전무이사 M24024 m24024@hanmaceng.co.kr
187 hglee3@hanmaceng.co.kr 이현구A 010-5240-2863 user construction-business 전무이사 M25028 odysseylee69@gmail.com
188 hiim@hanmaceng.co.kr 임호인 010-9984-8375 user construction-business 전무이사 M26008 m26008@hanmaceng.co.kr
189 gsjang@hanmaceng.co.kr 장길상 010-5402-8257 user construction-business 전무이사 M21452 m21452@hanmaceng.co.kr
190 esjeon@hanmaceng.co.kr 전은수 010-3664-5703 user construction-business 전무이사 M16401 m16401@hanmaceng.co.kr
191 jsjeon@hanmaceng.co.kr 전준수 010-3591-7011 user construction-business 전무이사 M16511 junsu0904@hanmaceng.co.kr
192 rgjeong@hanmaceng.co.kr 정락경 010-3205-3994 user construction-business 전무이사 M26015 m26015@hanmaceng.co.kr
193 sbjeong@hanmaceng.co.kr 정상범 010-8907-9883 user construction-business 전무이사 M17402 cjgh707@hanmaceng.co.kr
194 wcjeong2@hanmaceng.co.kr 정우창 010-6563-0034 user construction-business 전무이사 M25004 m25004@hanmaceng.co.kr
195 jhjeong3@hanmaceng.co.kr 정종호 010-5500-2532 user construction-business 전무이사 M23078 m23078@hanmaceng.co.kr
196 swcho2@hanmaceng.co.kr 조수원 010-3725-7539 user construction-business 전무이사 M26016 m26016@hanmaceng.co.kr
197 jgju@hanmaceng.co.kr 주재강 010-3690-8652 user construction-business 전무이사 M16313 joojkangg@hanmaceng.co.kr
198 hbjin@hanmaceng.co.kr 진현범 010-4365-2282 user construction-business 전무이사 M24070 m24070@hanmaceng.co.kr
199 ischae@hanmaceng.co.kr 채일석 010-2650-3232 user construction-business 전무이사 M23025 chae32@hanmaceng.co.kr
200 gjchoi@hanmaceng.co.kr 최규진 010-3856--5031 user construction-business 전무이사 M26034 -
201 hschoi3@hanmaceng.co.kr 최희섭 010-2680-2890 user construction-business 전무이사 M26011 m26011@hanmaceng.co.kr
202 sthan@hanmaceng.co.kr 한상태 010-7275-2709 user construction-business 전무이사 M23030 m23030@hanmaceng.co.kr
203 chheo@hanmaceng.co.kr 허찬행 010-7207-2339 user construction-business 전무이사 M21322 m21322@hanmaceng.co.kr
204 dihwang@hanmaceng.co.kr 황대익 010-3069-7430 user construction-business 전무이사 J05204 dihwang@hanmaceng.co.kr
205 smko@hanmaceng.co.kr 고석만 010-4995-3275 user construction-business 상무이사 M22030 m22030@hanmaceng.co.kr
206 ghkim7@hanmaceng.co.kr 김경환 010-8589-7560 user construction-business 상무이사 M17504 pintree0522@hanmaceng.co.kr
207 yskim13@hanmaceng.co.kr 김유식 010-8940-8772 user construction-business 상무이사 M26012 m26012@hanmaceng.co.kr
208 jykim10@hanmaceng.co.kr 김종용 010-9365-2389 user construction-business 상무이사 M25072 m25072@hanmaceng.co.kr
209 hgan@hanmaceng.co.kr 안흥권 010-3127-9962 user construction-business 상무이사 M26007 m26007@hanmaceng.co.kr
210 jsyang@hanmaceng.co.kr 양정선 010-9361-3569 user construction-business 상무이사 M20406 yangjs3569@hanmaceng.co.kr
211 ygyoon@hanmaceng.co.kr 윤여길 010-5658-9533 user construction-business 상무이사 M26009 m26009@hanmaceng.co.kr
212 glee2@hanmaceng.co.kr 이규 010-6339-9712 user construction-business 상무이사 M26027 m26027@hanmaceng.co.kr
213 swlee6@hanmaceng.co.kr 이선욱 010-5286-9782 user construction-business 전무이사 M02203 sul96@hanmaceng.co.kr
214 wlee@hanmaceng.co.kr 이웅 010-8604-1704 user construction-business 상무이사 M24054 m24054@hanmaceng.co.kr
215 jkchoi3@hanmaceng.co.kr 최정길 010-2240-9657 user construction-business 상무이사 M24064 m24064@hanmaceng.co.kr
216 jhchoi11@hanmaceng.co.kr 최종현 010-7416-7406 user construction-business 상무이사 M26030 m26030@hanmaceng.co.kr
217 yckang@hanmaceng.co.kr 강영철 010-5751-9279 user construction-business 이사 M25049 m25049@hanmaceng.co.kr
218 hskang@hanmaceng.co.kr 강현승 010-4662-6105 user construction-business 이사 M22077 m22077@hanmaceng.co.kr
219 dhkim7@hanmaceng.co.kr 김동한 010-6562-1795 user construction-business 이사 M25055 m25055@hanmaceng.co.kr
220 yskim11@hanmaceng.co.kr 김양석 010-4611-1307 user construction-business 이사 M23046 herykim@hanmaceng.co.kr
221 yskim12@hanmaceng.co.kr 김윤성 010-5718-0344 user construction-business 이사 M24007 rlays80@hanmaceng.co.kr
222 jmkim7@hanmaceng.co.kr 김정문 010-9899-3922 user construction-business 이사 M23008 m23008@hanmaceng.co.kr
223 sgoh@hanmaceng.co.kr 오세걸 010-8750-2827 user construction-business 이사 M26032 m26032@hanmaceng.co.kr
224 yglee2@hanmaceng.co.kr 이영규 010-3713-0529 user construction-business 이사 M23084 m23084@hanmaceng.co.kr
225 jslee5@hanmaceng.co.kr 이재성 010-3043-8848 user construction-business 이사 M25063 m25063@hanmaceng.co.kr
226 jblee@hanmaceng.co.kr 이종범 010-4046-3158 user construction-business 이사 M25048 m25048@hanmaceng.co.kr
227 jijung@hanmaceng.co.kr 정재익 010-4124-3452 user construction-business 이사 M05305 jjaeick@hanmaceng.co.kr
228 jhjeong2@hanmaceng.co.kr 정재혁 010-2300-0070 user construction-business 이사 M26019 m26019@hanmaceng.co.kr
229 ghcho@hanmaceng.co.kr 조규형 010-3357-8020 user construction-business 이사 M24072 m23023@hanmaceng.co.kr
230 sgchoi2@hanmaceng.co.kr 최성길 010-4332-0642 user construction-business 이사 M23031 m23031@hanmaceng.co.kr
231 jgchoi2@hanmaceng.co.kr 최재곤 010-9168-5632 user construction-business 이사 M25005 m25005@hanmaceng.co.kr
232 jwchoi7@hanmaceng.co.kr 최진우 010-6546-1121 user construction-business 이사 M24077 civil96@hanmaceng.co.kr
233 kskang2@hanmaceng.co.kr 강길성 010-8507-9840 user construction-business 부장 M26004 m26004@hanmaceng.co.kr
234 jhkim33@hanmaceng.co.kr 김정훈A 010-9437-8183 user construction-business 부장 M24067 jh067@hanmaceng.co.kr
235 igpark@hanmaceng.co.kr 박인규 010-2125-9748 user construction-business 부장 M22016 m21464@hanmaceng.co.kr
236 hcyang@hanmaceng.co.kr 양희찬 010-4501-0297 user construction-business 부장 M23065 m23065@hanmaceng.co.kr
237 jhyu2@hanmaceng.co.kr 유지훈 010-2842-4746 user construction-business 부장 M25069 m25069@hanmaceng.co.kr
238 dslee4@hanmaceng.co.kr 이동선 010-6369-7952 user construction-business 부장 M26020 m26020@hanmaceng.co.kr
239 jykoo@hanmaceng.co.kr 구자용 010-3042-1985 user construction-business 과장 M20311 jayong23@hanmaceng.co.kr
240 nhseo@hanmaceng.co.kr 서남호 010-2361-8585 user construction-business 과장 M23035 m23035@hanmaceng.co.kr
241 swryu@hanmaceng.co.kr 류시우 010-7554-5728 user construction-business 사원 M24057 m24057@hanmaceng.co.kr
242 cylee3@hanmaceng.co.kr 이창용 010-9304-4047 user safety-management 전무이사 M05508 lcy3392@hanmaceng.co.kr
243 mhjeong@hanmaceng.co.kr 정미희 010-5373-4697 user safety-management 대리 M22007 kokoball22@hanmaceng.co.kr
244 도로부 hklee3@hanmaceng.co.kr 이현구 010-8365-3205 user infrastructure-hq 부사장 본부장 M16222 hmokja@hanmaceng.co.kr
245 hslee6@hanmaceng.co.kr 이환섭 010-7763-5463 user infra-road 부사장 부서장 M22031 m22031@hanmaceng.co.kr
246 dhlee8@hanmaceng.co.kr 이동훈 010-7930-2170 user infra-road 전무 M03201 suny8561@hanmaceng.co.kr
247 yglee3@hanmaceng.co.kr 이영경 010-9016-3227 user infra-road 전무 M02213 yglee@hanmaceng.co.kr
248 kysong@hanmaceng.co.kr 송기영 010-8641-5769 user infra-road 상무 M07231 karet@hanmaceng.co.kr
249 kjlee@hanmaceng.co.kr 이기주 010-3347-2661 user infra-road 상무 M15207 lgj0906@hanmaceng.co.kr
250 cykim2@hanmaceng.co.kr 김정열 010-7239-2807 user infra-road 이사 J05215 comrade94@hanmaceng.co.kr
251 cskim@hanmaceng.co.kr 김창섭 010-2725-1074 user infra-road 이사 M22035 m22035@hanmaceng.co.kr
252 yjjeong2@hanmaceng.co.kr 정유종 010-4003-0329 user infra-road 이사 M25039 m25039@hanmaceng.co.kr
253 jwlee8@hanmaceng.co.kr 이정원 010-3741-0368 user infra-road 이사 M15206 with2u@hanmaceng.co.kr
254 csjeon@hanmaceng.co.kr 전찬성 010-6267-3181 user infra-road 이사 M07317 bravo635@hanmaceng.co.kr
255 kcpark@hanmaceng.co.kr 박기철 010-3572-7073 user infra-road 이사 M25020 m25020@hanmaceng.co.kr
256 mhahn@hanmaceng.co.kr 안민형 010-3559-3246 user infra-road 부장 M22003 m22003@hanmaceng.co.kr
257 twkim@hanmaceng.co.kr 김태우 010-7317-8668 user infra-road 과장 M20327 m20327@hanmaceng.co.kr
258 wylee@hanmaceng.co.kr 이원용 010-8562-7199 user infra-road 과장 M21460 m21460@hanmaceng.co.kr
259 cnamgung@hanmaceng.co.kr 남궁찬 010-9973-7314 user infra-road 과장 M22032 m22032@hanmaceng.co.kr
260 hoseo2@hanmaceng.co.kr 서현옥 010-6314-7382 user infra-road 과장 B15203 hyunok.seo@hanmaceng.co.kr
261 jwkim12@hanmaceng.co.kr 김종우 010-5924-2083 user infra-road 과장 M24082 m24082@hanmaceng.co.kr
262 mspark3@hanmaceng.co.kr 박민수 010-4056-0530 user infra-road 과장 M20305 msms1993@hanmaceng.co.kr
263 jhjin@hanmaceng.co.kr 진장현 010-7212-9015 user infra-road 과장 M23064 m23064@hanmaceng.co.kr
264 sikwon@hanmaceng.co.kr 권순일 010-8855-1279 user infra-road 대리 M21303 m21303@hanmaceng.co.kr
265 hjjeong2@hanmaceng.co.kr 정혜진 010-3706-1119 user infra-road 대리 M16317 hyejin@hanmaceng.co.kr
266 sbchoi2@hanmaceng.co.kr 최승범 010-2504-3509 user infra-road 대리 M21432 m21432@hanmaceng.co.kr
267 hjkim12@hanmaceng.co.kr 김효진 010-5510-3281 user infra-road 대리 M25038 m24020@hanmaceng.co.kr
268 ghlee6@hanmaceng.co.kr 이교호 010-2291-1996 user infra-road 사원 M23005 m23005@hanmaceng.co.kr
269 jhsong2@hanmaceng.co.kr 송주호 010-6691-5131 user infra-road 사원 M25007 m25007@hanmaceng.co.kr
270 jhju@hanmaceng.co.kr 주지한 010-9658-1289 user infra-road 사원 M25006 m25006@hanmaceng.co.kr
271 wkkim2@hanmaceng.co.kr 김웅기 010-6347-0667 user sales-support 부사장 M24050 wkikim@hanmaceng.co.kr
272 임원실 scjang@hanmaceng.co.kr 장석춘 010-9398-6896 user sales-support 부사장 M19101 jangchun18@naver.com
273 dskim4@hanmaceng.co.kr 김동수 010-5368-8504 user sales-support 사장 M25066 m25066@hanmaceng.co.kr
274 hjjang2@hanmaceng.co.kr 장혁진 010-8284-5434 user sales-support 부장 M25001 m25001@hanmaceng.co.kr
275 jdpark@hanmaceng.co.kr 박준동 010-6663-2312 user sales-support 부사장 M25074 m25074@hanmaceng.co.kr
276 bgahn@hanmaceng.co.kr 안병구 010-6353-7725 user sales-support 부장 M24071 m24071@hanmaceng.co.kr
277 nckim@hanmaceng.co.kr 김남철 010-8004-1523 user sales-support 부사장 M24004 m24004@hanmaceng.co.kr
278 thlee@hanmaceng.co.kr 이태환 010-5372-1458 user sales-support 부사장 M24053 ldy1474226@naver.com
279 djkim2@hanmaceng.co.kr 김대진 010-4820-0271 user sales-support 사장 M19104 dj279531@hanmaceng.co.kr
280 dhhan3@hanmaceng.co.kr 한동호 010-3841-1036 user sales-support 부사장 M25073 m25073@hanmaceng.co.kr
281 sclee3@hanmaceng.co.kr 이승철 010-3260-2533 user sales-support 부사장 M24008 m24008@hanmaceng.co.kr
282 shoh2@hanmaceng.co.kr 오세현 010-6505-8767 user sales-support 부사장 M24049 m24049@hanmaceng.co.kr
283 bjpark@hanmaceng.co.kr 박범진 010-7118-6193 user sales-support 부사장 M23002 bjpark9361@naver.com
284 mhseol@hanmaceng.co.kr 설문형 010-9901-0490 user sales-support 부사장 M24014 m24014@hanmaceng.co.kr
285 ockwon@hanmaceng.co.kr 권오춘 010-5382-3777 user sales-support 부사장 M21436 m21436@hanmaceng.co.kr
286 knlee@hanmaceng.co.kr 이규남 010-8568-2144 user sales-support 부사장 M21102 m21102@hanmaceng.co.kr
287 환경 sskim7@hanmaceng.co.kr 김성수 010-8745-3105 user land-env-assessment 부사장 M12204 m12204@hanmaceng.co.kr
288 sjkang3@hanmaceng.co.kr 강선준 010-4056-6885 user land-env-assessment 부사장 M09104 sjkang1226@hanmaceng.co.kr
289 bdlee@hanmaceng.co.kr 이병도 010-5166-9329 user land-env-assessment 전무 부서장 M20101 bottleroad@hanmaceng.co.kr
290 cwlee2@hanmaceng.co.kr 이찬우 010-6403-0242 user land-env-assessment 전무 M05207 lchanw@hanmaceng.co.kr
291 jwlee7@hanmaceng.co.kr 이정우 010-9408-9042 user land-env-assessment 상무 M23071 m23071@hanmaceng.co.kr
292 bwlee2@hanmaceng.co.kr 이병욱 010-2406-6787 user land-env-assessment 이사 M16216 lbw002@hanmaceng.co.kr
293 djlee2@hanmaceng.co.kr 이동준 010-4447-1980 user land-env-assessment 이사 M20205 djlee7164@hanmaceng.co.kr
294 sikim@hanmaceng.co.kr 김승일 010-2769-2104 user land-env-assessment 이사 M21434 ryan23@hanmaceng.co.kr
295 bcseo@hanmaceng.co.kr 서병철 010-3760-2579 user land-env-assessment 부장 M12207 mintwiz@hanmaceng.co.kr
296 yhkim@hanmaceng.co.kr 김용현 010-7156-1174 user land-env-assessment 차장 M18205 kimyh1174@hanmaceng.co.kr
297 ghlee5@hanmaceng.co.kr 이가형 010-6471-6300 user land-env-assessment 차장 M22001 m22001@hanmaceng.co.kr
298 dyjung2@hanmaceng.co.kr 정도영 010-4882-5145 user land-env-assessment 차장 M22074 jdy5145@hanmaceng.co.kr
299 hjcho2@hanmaceng.co.kr 조현준 010-4797-6247 user land-env-assessment 과장 M23022 m23022@hanmaceng.co.kr
300 mspark2@hanmaceng.co.kr 박민상 010-4389-8666 user land-env-assessment 과장 M18308 minsang404@hanmaceng.co.kr
301 smhong@hanmaceng.co.kr 홍세민 010-5231-6026 user land-env-assessment 과장 M20208 tpals32@hanmaceng.co.kr
302 yskim10@hanmaceng.co.kr 김용석 010-9991-6082 user land-env-assessment 과장 M19329 kyss6082@hanmaceng.co.kr
303 hjpark2@hanmaceng.co.kr 박호진 010-9257-8917 user land-env-assessment 대리 M22064 m22064@hanmaceng.co.kr
304 hglee2@hanmaceng.co.kr 이현건 010-2994-4516 user land-env-assessment 대리 M22067 m22067@hanmaceng.co.kr
305 sjyu@hanmaceng.co.kr 유수지 010-4191-2018 user land-env-assessment 대리 M22056 m22056@hanmaceng.co.kr
306 jkchoi2@hanmaceng.co.kr 최진경 010-7412-3248 user land-env-assessment 사원 M24043 m24043@hanmaceng.co.kr