From c1d0f4c1dbc6a44bfbef11cb487952581e73b82c Mon Sep 17 00:00:00 2001 From: how2ice Date: Tue, 26 May 2026 12:26:33 +0900 Subject: [PATCH] feat: refresh shared chat and server workflows --- .../server-command/restart-work-server.sh | 172 +- etc/servers/work-server/.gitignore | 1 + etc/servers/work-server/README.md | 8 +- .../work-server/data/e-reader-library.json | 503 +- etc/servers/work-server/docker-compose.yml | 71 +- etc/servers/work-server/src/app.ts | 44 + etc/servers/work-server/src/config/env.ts | 1 + .../src/routes/baseball-ticket-bay.ts | 205 + etc/servers/work-server/src/routes/chat.ts | 447 +- etc/servers/work-server/src/routes/health.ts | 27 +- etc/servers/work-server/src/routes/runtime.ts | 42 + .../work-server/src/routes/server-command.ts | 83 +- etc/servers/work-server/src/server.ts | 4 + .../services/baseball-ticket-bay-service.ts | 1449 +++ .../src/services/chat-message-parts.ts | 58 + .../src/services/chat-room-service.test.ts | 11 + .../src/services/chat-room-service.ts | 216 + .../work-server/src/services/chat-service.ts | 54 + .../src/services/git-service.test.ts | 21 +- .../work-server/src/services/git-service.ts | 23 +- .../src/services/notification-service.ts | 12 + .../services/runtime-drain-service.test.ts | 44 + .../src/services/runtime-drain-service.ts | 56 + .../services/server-command-service.test.ts | 18 +- .../src/services/server-command-service.ts | 67 +- .../server-restart-reservation-service.ts | 101 +- .../src/workers/baseball-ticket-bay-worker.ts | 57 + src/App.tsx | 63 +- src/app/main/AppShell.tsx | 3 +- src/app/main/ChatNotificationBridgeV2.tsx | 2 +- src/app/main/MainChatPanel.tsx | 185 +- src/app/main/MainContent.tsx | 4 - src/app/main/MainHeader.tsx | 238 +- src/app/main/PlayAppOverlay.tsx | 5 + src/app/main/ScopedChatRoomsWindow.css | 245 - src/app/main/ScopedChatRoomsWindow.tsx | 152 - src/app/main/SharedAppSettingsPage.tsx | 3 - src/app/main/SharedChatManagementPage.tsx | 14 +- src/app/main/SharedResourceManagementPage.tsx | 6 +- src/app/main/SystemChatPage.css | 2432 +++++ src/app/main/SystemChatPage.tsx | 915 ++ src/app/main/SystemChatPanel.css | 1064 -- src/app/main/SystemChatPanel.hotfix.css | 3 - src/app/main/SystemChatPanel.tsx | 9528 ----------------- src/app/main/TokenSettingManagementPage.tsx | 2 +- src/app/main/chatSessionRouting.ts | 13 + .../components/ConversationListPane.tsx | 4 +- .../useConversationViewportController.ts | 50 +- src/app/main/chatWindowActions.ts | 3 + src/app/main/layout/MainLayout.tsx | 75 +- src/app/main/layout/buildSearchOptions.ts | 24 +- .../mainChatPanel/ChatConversationView.tsx | 432 +- src/app/main/mainChatPanel/ChatPromptCard.tsx | 156 +- src/app/main/mainChatPanel/chatUtils.ts | 42 + src/app/main/mainChatPanel/messageParts.ts | 48 + .../styles/MainChatPanel.conversation.css | 41 + .../styles/MainChatPanel.preview-runtime.css | 13 + src/app/main/mainChatPanel/types.ts | 20 +- src/app/main/mainView/searchOptions.ts | 12 - src/app/main/notificationApi.ts | 27 + src/app/main/pages/ChatPage.tsx | 6 +- src/app/main/pages/ChatSharePage.css | 264 +- src/app/main/pages/ChatSharePage.tsx | 1284 ++- src/app/main/previewRuntime.ts | 18 +- src/app/main/routes.tsx | 8 +- .../styles/SystemChatPanel.rooms-shared.css | 311 - .../MainChatPanel.conversation.css | 2465 +++++ .../MainChatPanel.preview-runtime.css | 2375 ++++ src/app/main/tokenAccess.ts | 38 +- src/app/main/webPushRegistration.ts | 208 + .../serverCommand/ServerCommandPage.tsx | 564 +- src/features/serverCommand/api.ts | 47 +- src/features/serverCommand/serverCommand.css | 239 + src/sw.js | 170 + src/views/play/apps/apps/AppsLibraryView.css | 6 + src/views/play/apps/apps/AppsLibraryView.tsx | 7 + src/views/play/apps/apps/appsRegistry.tsx | 19 + .../BaseballTicketBayPlayAppView.css | 1072 ++ .../BaseballTicketBayPlayAppView.tsx | 2016 ++++ .../baseballTicketBayApi.ts | 253 + .../play/apps/e-reader/EReaderAppView.tsx | 50 +- src/views/play/apps/e-reader/eReaderApi.ts | 26 + 82 files changed, 18604 insertions(+), 12461 deletions(-) mode change 100644 => 100755 etc/commands/server-command/restart-work-server.sh create mode 100644 etc/servers/work-server/src/routes/baseball-ticket-bay.ts create mode 100644 etc/servers/work-server/src/routes/runtime.ts create mode 100644 etc/servers/work-server/src/services/baseball-ticket-bay-service.ts create mode 100644 etc/servers/work-server/src/services/runtime-drain-service.test.ts create mode 100644 etc/servers/work-server/src/services/runtime-drain-service.ts create mode 100644 etc/servers/work-server/src/workers/baseball-ticket-bay-worker.ts delete mode 100644 src/app/main/ScopedChatRoomsWindow.css delete mode 100644 src/app/main/ScopedChatRoomsWindow.tsx create mode 100644 src/app/main/SystemChatPage.css create mode 100644 src/app/main/SystemChatPage.tsx delete mode 100644 src/app/main/SystemChatPanel.css delete mode 100644 src/app/main/SystemChatPanel.hotfix.css delete mode 100644 src/app/main/SystemChatPanel.tsx create mode 100644 src/app/main/chatSessionRouting.ts create mode 100644 src/app/main/chatWindowActions.ts delete mode 100644 src/app/main/systemChat/styles/SystemChatPanel.rooms-shared.css create mode 100644 src/app/main/systemChatStyles/MainChatPanel.conversation.css create mode 100644 src/app/main/systemChatStyles/MainChatPanel.preview-runtime.css create mode 100644 src/app/main/webPushRegistration.ts create mode 100644 src/views/play/apps/baseball-ticket-bay/BaseballTicketBayPlayAppView.css create mode 100644 src/views/play/apps/baseball-ticket-bay/BaseballTicketBayPlayAppView.tsx create mode 100644 src/views/play/apps/baseball-ticket-bay/baseballTicketBayApi.ts diff --git a/etc/commands/server-command/restart-work-server.sh b/etc/commands/server-command/restart-work-server.sh old mode 100644 new mode 100755 index 7179c89..77d7d95 --- a/etc/commands/server-command/restart-work-server.sh +++ b/etc/commands/server-command/restart-work-server.sh @@ -5,7 +5,177 @@ set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) COMPOSE_FILE="$REPO_ROOT/etc/servers/work-server/docker-compose.yml" +PROXY_SERVICE="${WORK_SERVER_PROXY_SERVICE:-work-server}" +PROXY_CONTAINER="${WORK_SERVER_PROXY_CONTAINER:-work-server}" +BLUE_SERVICE="${WORK_SERVER_BLUE_SERVICE:-work-server-blue}" +GREEN_SERVICE="${WORK_SERVER_GREEN_SERVICE:-work-server-green}" +BLUE_CONTAINER="${WORK_SERVER_BLUE_CONTAINER:-work-server-blue}" +GREEN_CONTAINER="${WORK_SERVER_GREEN_CONTAINER:-work-server-green}" +ACTIVE_SLOT_FILE="${WORK_SERVER_ACTIVE_SLOT_FILE:-$REPO_ROOT/etc/servers/work-server/.docker/runtime/active-slot}" +PROXY_CONFIG_FILE="${WORK_SERVER_PROXY_CONFIG_FILE:-$REPO_ROOT/etc/servers/work-server/.docker/proxy/default.conf}" +HEALTH_ENDPOINT="${WORK_SERVER_HEALTH_ENDPOINT:-http://127.0.0.1:3100/health}" +RUNTIME_ENDPOINT="${WORK_SERVER_RUNTIME_ENDPOINT:-http://127.0.0.1:3100/api/runtime}" +PREVIOUS_SLOT_DRAIN_TIMEOUT_SECONDS="${WORK_SERVER_PREVIOUS_SLOT_DRAIN_TIMEOUT_SECONDS:-900}" cd "$REPO_ROOT" -exec docker compose -f "$COMPOSE_FILE" up -d --build --force-recreate --no-deps work-server +mkdir -p "$(dirname "$ACTIVE_SLOT_FILE")" "$(dirname "$PROXY_CONFIG_FILE")" + +read_active_slot() { + if [ -f "$ACTIVE_SLOT_FILE" ]; then + SLOT=$(tr -d '[:space:]' <"$ACTIVE_SLOT_FILE") + if [ "$SLOT" = "blue" ] || [ "$SLOT" = "green" ]; then + printf '%s' "$SLOT" + return 0 + fi + fi + + printf 'blue' +} + +container_is_running() { + CONTAINER_NAME="$1" + STATUS=$(docker inspect -f '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || true) + [ "$STATUS" = "running" ] +} + +resolve_active_slot() { + SLOT=$(read_active_slot) + + if [ "$SLOT" = "blue" ] && ! container_is_running "$BLUE_CONTAINER" && container_is_running "$GREEN_CONTAINER"; then + printf 'green' + return 0 + fi + + if [ "$SLOT" = "green" ] && ! container_is_running "$GREEN_CONTAINER" && container_is_running "$BLUE_CONTAINER"; then + printf 'blue' + return 0 + fi + + printf '%s' "$SLOT" +} + +write_proxy_config() { + SLOT="$1" + TARGET_CONTAINER="$BLUE_CONTAINER" + + if [ "$SLOT" = "green" ]; then + TARGET_CONTAINER="$GREEN_CONTAINER" + fi + + cat >"$PROXY_CONFIG_FILE" < { if (!response.ok) process.exit(1); process.stdout.write(await response.text()); }).catch(() => process.exit(1));" "$HEALTH_ENDPOINT" >/dev/null 2>&1; then + return 0 + fi + + ATTEMPT=$((ATTEMPT + 1)) + sleep 2 + done + + echo "health check failed for $TARGET_CONTAINER" >&2 + return 1 +} + +read_runtime_value() { + TARGET_CONTAINER="$1" + FIELD_NAME="$2" + + docker exec "$TARGET_CONTAINER" node -e "fetch(process.argv[1]).then((response) => response.json()).then((payload) => { const value = payload?.[process.argv[2]]; if (typeof value === 'boolean') { process.stdout.write(value ? 'true' : 'false'); return; } if (value == null) { process.stdout.write(''); return; } process.stdout.write(String(value)); }).catch(() => process.exit(1));" "$RUNTIME_ENDPOINT" "$FIELD_NAME" +} + +set_container_draining() { + TARGET_CONTAINER="$1" + DRAINING_VALUE="$2" + + docker exec "$TARGET_CONTAINER" node -e "fetch(process.argv[1], { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ draining: process.argv[2] === 'true' }) }).then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1));" "${RUNTIME_ENDPOINT}/drain" "$DRAINING_VALUE" +} + +wait_for_previous_slot_drain() { + TARGET_CONTAINER="$1" + ELAPSED=0 + + while [ "$ELAPSED" -lt "$PREVIOUS_SLOT_DRAIN_TIMEOUT_SECONDS" ]; do + ACTIVE_COUNT=$(read_runtime_value "$TARGET_CONTAINER" activeChatRequestCount 2>/dev/null || printf '0') + QUEUED_COUNT=$(read_runtime_value "$TARGET_CONTAINER" queuedChatRequestCount 2>/dev/null || printf '0') + + if [ "${ACTIVE_COUNT:-0}" = "0" ] && [ "${QUEUED_COUNT:-0}" = "0" ]; then + return 0 + fi + + sleep 2 + ELAPSED=$((ELAPSED + 2)) + done + + echo "drain timeout reached for $TARGET_CONTAINER" >&2 + return 1 +} + +ensure_proxy_running() { + docker compose -f "$COMPOSE_FILE" up -d --no-deps "$PROXY_SERVICE" >/dev/null + docker exec "$PROXY_CONTAINER" nginx -s reload >/dev/null +} + +ACTIVE_SLOT=$(resolve_active_slot) +TARGET_SLOT="green" +TARGET_SERVICE="$GREEN_SERVICE" +TARGET_CONTAINER="$GREEN_CONTAINER" +PREVIOUS_SERVICE="$BLUE_SERVICE" +PREVIOUS_CONTAINER="$BLUE_CONTAINER" + +if [ "$ACTIVE_SLOT" = "green" ]; then + TARGET_SLOT="blue" + TARGET_SERVICE="$BLUE_SERVICE" + TARGET_CONTAINER="$BLUE_CONTAINER" + PREVIOUS_SERVICE="$GREEN_SERVICE" + PREVIOUS_CONTAINER="$GREEN_CONTAINER" +fi + +docker compose -f "$COMPOSE_FILE" up -d --build --force-recreate --no-deps "$TARGET_SERVICE" +wait_for_container_health "$TARGET_CONTAINER" +write_proxy_config "$TARGET_SLOT" +ensure_proxy_running +printf '%s\n' "$TARGET_SLOT" >"$ACTIVE_SLOT_FILE" + +if [ "$PREVIOUS_SERVICE" != "$TARGET_SERVICE" ]; then + set_container_draining "$PREVIOUS_CONTAINER" true + wait_for_previous_slot_drain "$PREVIOUS_CONTAINER" + docker compose -f "$COMPOSE_FILE" up -d --build --force-recreate --no-deps "$PREVIOUS_SERVICE" + wait_for_container_health "$PREVIOUS_CONTAINER" +fi + +printf 'work-server zero-downtime switch completed: %s -> %s\n' "$ACTIVE_SLOT" "$TARGET_SLOT" diff --git a/etc/servers/work-server/.gitignore b/etc/servers/work-server/.gitignore index 9c97bbd..0ee3fe3 100755 --- a/etc/servers/work-server/.gitignore +++ b/etc/servers/work-server/.gitignore @@ -1,3 +1,4 @@ node_modules dist +.dist-verify-actual .env diff --git a/etc/servers/work-server/README.md b/etc/servers/work-server/README.md index 5eda1d4..bd26265 100644 --- a/etc/servers/work-server/README.md +++ b/etc/servers/work-server/README.md @@ -17,7 +17,13 @@ docker compose up -d docker compose logs -f work-server ``` -`work-server`는 HMR/watch 없이 빌드 산출물(`dist`)을 실행합니다. 컨테이너 재기동은 `docker compose up -d --build --force-recreate --no-deps work-server` 기준으로 최신 소스를 다시 빌드한 뒤 새 컨테이너를 띄웁니다. +`work-server`는 `3100` 포트를 점유하는 nginx 프록시이고, 실제 API 런타임은 `work-server-blue` / `work-server-green` 슬롯으로 동작합니다. 재기동은 비활성 슬롯을 먼저 새로 빌드해 `/health` 확인 후 프록시 업스트림을 전환하고, 마지막에 이전 슬롯을 내리는 방식으로 무중단 전환합니다. + +슬롯 로그까지 같이 보려면 아래처럼 확인합니다. + +```bash +docker compose logs -f work-server work-server-blue work-server-green +``` 호스트 프로젝트 루트와 동일한 문맥으로 서버 재기동을 처리하려면 별도 host runner를 사용합니다. 이 runner는 별도 명시적 요청이 있을 때만 수동으로 켜거나 재기동합니다. diff --git a/etc/servers/work-server/data/e-reader-library.json b/etc/servers/work-server/data/e-reader-library.json index bfb1e78..b1468f3 100644 --- a/etc/servers/work-server/data/e-reader-library.json +++ b/etc/servers/work-server/data/e-reader-library.json @@ -1,5 +1,485 @@ { "items": [ + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-91ff53bf87e1a1a3", + "title": "\"도 넘었다\"…이스라엘 극우 장관, 가자 활동가 조롱에 국제사회 반발", + "sourceLabel": "아시아경제", + "url": "https://n.news.naver.com/mnews/article/277/0005765972", + "lead": "이스라엘, 구호선 활동가 400명 구금 극우 장관, 무릎 꿇린 활동가들 조롱해 유럽 각국 반발…李, 활동가 억류 규탄 이스라엘 집권 연정의 극우 성향 정치인이 동지중해에서 나포한 가자지구 구호선단 활동가들을 조롱하는 영상을 공개해 파장이 일고 있다.", + "body": "이스라엘, 구호선 활동가 400명 구금 극우 장관, 무릎 꿇린 활동가들 조롱해 유럽 각국 반발…李, 활동가 억류 규탄 이스라엘 집권 연정의 극우 성향 정치인이 동지중해에서 나포한 가자지구 구호선단 활동가들을 조롱하는 영상을 공개해 파장이 일고 있다.\n\n이타마르 벤그비르 이스라엘 국가안보장관이 억류된 국제 활동가들을 찾아가 조롱하는 영상을 올렸다. 엑스\n\n연합뉴스에 따르면 20일(현지시간) 이타마르 벤그비르 이스라엘 국가안보 장관은 억류된 국제 활동가들을 찾아가 조롱하는 영상을 자신의 사회관계망서비스(SNS) 계정에 직접 공개했다. 영상에는 수십명의 활동가들이 마치 현행범처럼 아스돗 항구 구금 시설 바닥에 손이 묶인 채 무릎을 꿇고 있는 모습이 담겼다.\n\n해당 영상은 한 활동가가 \"팔레스타인에 자유를\"이라고 외치는 장면으로 시작된다. 경비 대원들은 이 활동가의 머리를 손바닥으로 눌러 내리고 거칠게 끌고 나갔다. 이후 벤그비르 장관은 수갑을 찬 채 무릎을 꿇고 있는 활동가들 앞에서 대형 이스라엘 국기를 흔들며 히브리어로 \"이스라엘에 온 것을 환영한다, 우리가 바로 이 땅의 주인이다\"라고 외쳤다. 또 다른 활동가가 강제 제압당하는 장면을 두고는 \"원래 이래야 마땅하다\"고 말하는 장면도 포함됐다.\n\n억류된 활동가들은 지난주 튀르키예에서 출항해 이스라엘의 해상 봉쇄를 뚫으려던 구호 선단 탑승자들이다. 이스라엘 외무부가 전날 \"봉쇄 돌파 시도가 종료됐다\"고 발표한 뒤 이스라엘군에 의해 아스돗 항구로 강제 압송됐으며, 현재 400여 명이 구금 중이다.\n\n이스라엘 해군이 지난 19일(현지시간) 지중해에서 압수한 가자행 함대 함정을 요격한 후 아쉬도드 항구로 항해하고 있는 모습. AP연합뉴스\n\n국제사회에서는 비판이 잇따랐다. 특히 구금된 활동가 중에는 캐서린 코널리 아일랜드 대통령의 자매 마거릿 코널리 박사를 비롯한 아일랜드인 12명이 포함된 것으로 전해졌다. 코널리 대통령은 전날 영국 방문 중 \"아주 속상한 일\"이라며 \"마거릿이 자랑스럽지만, 걱정도 많이 된다\"고 말했다. 미하일 마틴 아일랜드 총리는 \"활동가 구금은 용납할 수 없고 잘못된 일\"이라고 강조했고, 헬렌 메켄티 아일랜드 외무장관도 \"끔찍하다\"며 즉각 석방을 촉구했다.\n\n유럽 각국도 이스라엘 대사를 초치하며 항의했다. 이베트 쿠퍼 영국 외무장관은 자신의 SNS에 \"완전히 경악했다\"며 \"이스라엘 당국에 설명을 요구했다\"고 밝혔다. 장노엘 바로 프랑스 외무장관은 \"분노를 표현하고 설명을 듣기 위해 이스라엘 대사 소환을 요구했다\"고 했고, 호세 마누엘 알바레스 스페인 외무장관은 \"끔찍하고 수치스러우며 비인도적인 처사\"라고 규탄했다. 독일·그리스·이탈리아·네덜란드·포르투갈·캐나다·튀르키예도 비판 대열에 합류했다.\n\n논란이 커지자 베냐민 네타냐후 이스라엘 총리는 이례적으로 벤그비르 장관을 비판했다. 네타냐후 총리는 \"벤그비르 장관이 구호선 활동가들을 대하고 다룬 방식은 이스라엘의 가치 및 규범과 전혀 부합하지 않는다\"고 밝혔다. 이어 \"외교적 파장이 커지는 것을 막기 위해 관련 부처에 즉각적인 조치를 명령했다\"며 \"(활동가들을) 이른 시일 내에 이스라엘 영토 밖으로 강제 추방하라\"고 지시했다.\n\n한편 한국인 활동가가 탑승한 가자 구호선이 이스라엘군에 나포된 것을 두고 국내에서도 비판의 목소리가 나왔다. 현재 한국인 활동가 2명(김동현·김아현)이 억류된 상태로, 외교부는 이스라엘 측에 조속한 석방·추방을 요청했다.\n\n이재명 대통령은 20일 국무회의에서 \"우리 국민을 국제법적으로 타당하지 않은 사유로 잡아간 것은 너무 심하고 비인도적\"이라며 \"최소한의 국제 규범이라는 게 있는데 다 어기고 있다. 원칙대로 하라, 너무 많이 인내했다\"고 밝혔다. 아울러 국제형사재판소(ICC)가 발부한 네타냐후 총리 체포영장 집행 문제에 대해 \"우리도 판단해보자\"고 말해 외교적 파장이 예상된다.", + "htmlBody": "

이스라엘, 구호선 활동가 400명 구금 극우 장관, 무릎 꿇린 활동가들 조롱해 유럽 각국 반발…李, 활동가 억류 규탄 이스라엘 집권 연정의 극우 성향 정치인이 동지중해에서 나포한 가자지구 구호선단 활동가들을 조롱하는 영상을 공개해 파장이 일고 있다.

\"이타마르

이타마르 벤그비르 이스라엘 국가안보장관이 억류된 국제 활동가들을 찾아가 조롱하는 영상을 올렸다. 엑스

연합뉴스에 따르면 20일(현지시간) 이타마르 벤그비르 이스라엘 국가안보 장관은 억류된 국제 활동가들을 찾아가 조롱하는 영상을 자신의 사회관계망서비스(SNS) 계정에 직접 공개했다. 영상에는 수십명의 활동가들이 마치 현행범처럼 아스돗 항구 구금 시설 바닥에 손이 묶인 채 무릎을 꿇고 있는 모습이 담겼다.

해당 영상은 한 활동가가 "팔레스타인에 자유를"이라고 외치는 장면으로 시작된다. 경비 대원들은 이 활동가의 머리를 손바닥으로 눌러 내리고 거칠게 끌고 나갔다. 이후 벤그비르 장관은 수갑을 찬 채 무릎을 꿇고 있는 활동가들 앞에서 대형 이스라엘 국기를 흔들며 히브리어로 "이스라엘에 온 것을 환영한다, 우리가 바로 이 땅의 주인이다"라고 외쳤다. 또 다른 활동가가 강제 제압당하는 장면을 두고는 "원래 이래야 마땅하다"고 말하는 장면도 포함됐다.

억류된 활동가들은 지난주 튀르키예에서 출항해 이스라엘의 해상 봉쇄를 뚫으려던 구호 선단 탑승자들이다. 이스라엘 외무부가 전날 "봉쇄 돌파 시도가 종료됐다"고 발표한 뒤 이스라엘군에 의해 아스돗 항구로 강제 압송됐으며, 현재 400여 명이 구금 중이다.

\"이스라엘

이스라엘 해군이 지난 19일(현지시간) 지중해에서 압수한 가자행 함대 함정을 요격한 후 아쉬도드 항구로 항해하고 있는 모습. AP연합뉴스

국제사회에서는 비판이 잇따랐다. 특히 구금된 활동가 중에는 캐서린 코널리 아일랜드 대통령의 자매 마거릿 코널리 박사를 비롯한 아일랜드인 12명이 포함된 것으로 전해졌다. 코널리 대통령은 전날 영국 방문 중 "아주 속상한 일"이라며 "마거릿이 자랑스럽지만, 걱정도 많이 된다"고 말했다. 미하일 마틴 아일랜드 총리는 "활동가 구금은 용납할 수 없고 잘못된 일"이라고 강조했고, 헬렌 메켄티 아일랜드 외무장관도 "끔찍하다"며 즉각 석방을 촉구했다.

유럽 각국도 이스라엘 대사를 초치하며 항의했다. 이베트 쿠퍼 영국 외무장관은 자신의 SNS에 "완전히 경악했다"며 "이스라엘 당국에 설명을 요구했다"고 밝혔다. 장노엘 바로 프랑스 외무장관은 "분노를 표현하고 설명을 듣기 위해 이스라엘 대사 소환을 요구했다"고 했고, 호세 마누엘 알바레스 스페인 외무장관은 "끔찍하고 수치스러우며 비인도적인 처사"라고 규탄했다. 독일·그리스·이탈리아·네덜란드·포르투갈·캐나다·튀르키예도 비판 대열에 합류했다.

논란이 커지자 베냐민 네타냐후 이스라엘 총리는 이례적으로 벤그비르 장관을 비판했다. 네타냐후 총리는 "벤그비르 장관이 구호선 활동가들을 대하고 다룬 방식은 이스라엘의 가치 및 규범과 전혀 부합하지 않는다"고 밝혔다. 이어 "외교적 파장이 커지는 것을 막기 위해 관련 부처에 즉각적인 조치를 명령했다"며 "(활동가들을) 이른 시일 내에 이스라엘 영토 밖으로 강제 추방하라"고 지시했다.

\"이재명

한편 한국인 활동가가 탑승한 가자 구호선이 이스라엘군에 나포된 것을 두고 국내에서도 비판의 목소리가 나왔다. 현재 한국인 활동가 2명(김동현·김아현)이 억류된 상태로, 외교부는 이스라엘 측에 조속한 석방·추방을 요청했다.

이재명 대통령은 20일 국무회의에서 "우리 국민을 국제법적으로 타당하지 않은 사유로 잡아간 것은 너무 심하고 비인도적"이라며 "최소한의 국제 규범이라는 게 있는데 다 어기고 있다. 원칙대로 하라, 너무 많이 인내했다"고 밝혔다. 아울러 국제형사재판소(ICC)가 발부한 네타냐후 총리 체포영장 집행 문제에 대해 "우리도 판단해보자"고 말해 외교적 파장이 예상된다.

", + "tags": [ + "네이버뉴스", + "정치" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-20T23:31:18.000Z" + }, + "createdAt": "2026-05-26T00:20:48.312Z", + "updatedAt": "2026-05-26T00:20:48.312Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-71848db0cb313f1d", + "title": "\"장벽·차별 없는 박물관\"…문체부", + "sourceLabel": "뉴시스", + "url": "https://n.news.naver.com/mnews/article/003/0013958723", + "lead": "'전시정보 수어 영상 제작 지원 사업' 공모 박물관·미술관 대상…상설 전시 보유시 우선 6월 12일까지 공모…6월22일 최종 10곳 선정", + "body": "'전시정보 수어 영상 제작 지원 사업' 공모 박물관·미술관 대상…상설 전시 보유시 우선 6월 12일까지 공모…6월22일 최종 10곳 선정\n\n[서울=뉴시스] 유금와당박물관 전시물 소개 수어 영상 (사진=문화체육관광부 제공) 2026.05.21. photo@newsis.com *재판매 및 DB 금지\n\n[서울=뉴시스]한이재 기자 = 문화체육관광부가 전시 정보 수어 영상 제작 지원 사업에 참여할 전시기관을 찾는다.\n\n문체부는 21일 농인의 문화·여가 향유 기회를 확대하고 전시 관람 접근성을 높이기 위해 이날부터 내달 12일까지 지원 사업 참여기관 10개소를 공모한다고 밝혔다.\n\n지원 취지는 모두가 이해하고 즐기는 전시를 만드는 것이다.\n\n공모 대상은 전국의 박물관, 미술관, 전시관, 기념관 등 전시 콘텐츠를 보유한 모든 기관이다.\n\n다만, 영상의 지속 활용을 위해 상설 전시나 장기 운영 전시 보유 기관을 우선한다.\n\n문체부는 농인의 관람 수요를 조사하고 제작 필요성을 검토할 예정이라고 밝혔다.\n\n또 지역 균형과 시설 접근성, 전시 적합성 등도 살핀다.\n\n[서울=뉴시스] '전시정보 수어 영상 제작 지원 사업' 홍보물 (사진=문화체육관광부 제공) 2026.05.21. photo@newsis.com *재판매 및 DB 금지\n\n지난 3년간 제작된 수어 영상 콘텐츠 총 882편은 공식 누리소통망 계정을 통해 확인할 수 있다.\n\n참여 신청은 '2026 전시 정보 수어 영상 제작 지원 사업' 누리집에서 할 수 있다.\n\n신청서 기본 요건 검토, 농인 수요도 조사 결과, 종합 심사 등을 걸쳐 내달 22일 최종 선정 기관을 발표한다.\n\n문체부 정책 담당자는 \"농인들의 실제 목소리를 반영해 가장 필요하고 원하는 곳에 수어 영상이 보급될 수 있도록 노력하겠다\"며 \"모두에게 장벽 없는 문화예술을 누릴 수 있는 환경을 만드는 데 관심 있는 전시 기관들의 많은 참여를 바란다\"라고 전했다.", + "htmlBody": "

'전시정보 수어 영상 제작 지원 사업' 공모 박물관·미술관 대상…상설 전시 보유시 우선 6월 12일까지 공모…6월22일 최종 10곳 선정

\"[서울=뉴시스]

[서울=뉴시스] 유금와당박물관 전시물 소개 수어 영상 (사진=문화체육관광부 제공) 2026.05.21. photo@newsis.com *재판매 및 DB 금지

[서울=뉴시스]한이재 기자 = 문화체육관광부가 전시 정보 수어 영상 제작 지원 사업에 참여할 전시기관을 찾는다.

문체부는 21일 농인의 문화·여가 향유 기회를 확대하고 전시 관람 접근성을 높이기 위해 이날부터 내달 12일까지 지원 사업 참여기관 10개소를 공모한다고 밝혔다.

지원 취지는 모두가 이해하고 즐기는 전시를 만드는 것이다.

공모 대상은 전국의 박물관, 미술관, 전시관, 기념관 등 전시 콘텐츠를 보유한 모든 기관이다.

다만, 영상의 지속 활용을 위해 상설 전시나 장기 운영 전시 보유 기관을 우선한다.

문체부는 농인의 관람 수요를 조사하고 제작 필요성을 검토할 예정이라고 밝혔다.

또 지역 균형과 시설 접근성, 전시 적합성 등도 살핀다.

\"[서울=뉴시스]

[서울=뉴시스] '전시정보 수어 영상 제작 지원 사업' 홍보물 (사진=문화체육관광부 제공) 2026.05.21. photo@newsis.com *재판매 및 DB 금지

지난 3년간 제작된 수어 영상 콘텐츠 총 882편은 공식 누리소통망 계정을 통해 확인할 수 있다.

참여 신청은 '2026 전시 정보 수어 영상 제작 지원 사업' 누리집에서 할 수 있다.

신청서 기본 요건 검토, 농인 수요도 조사 결과, 종합 심사 등을 걸쳐 내달 22일 최종 선정 기관을 발표한다.

문체부 정책 담당자는 "농인들의 실제 목소리를 반영해 가장 필요하고 원하는 곳에 수어 영상이 보급될 수 있도록 노력하겠다"며 "모두에게 장벽 없는 문화예술을 누릴 수 있는 환경을 만드는 데 관심 있는 전시 기관들의 많은 참여를 바란다"라고 전했다.

", + "tags": [ + "네이버뉴스", + "생활/문화" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T00:18:27.000Z" + }, + "createdAt": "2026-05-26T00:20:47.671Z", + "updatedAt": "2026-05-26T00:20:47.671Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-772a00d4b6b5836f", + "title": "이달에만 두 번째···하동 레일바이크 타다 1명 사망·3명 부상", + "sourceLabel": "동아일보", + "url": "https://n.news.naver.com/mnews/article/020/0003721342", + "lead": "뉴시스 경남 하동에서 레일바이크를 타던 70대 이용객이 숨졌다. 보름 사이 두 차례 추돌사고가 잇따라 발생하면서 경찰은 본격 수사에 착수했다.", + "body": "뉴시스 경남 하동에서 레일바이크를 타던 70대 이용객이 숨졌다. 보름 사이 두 차례 추돌사고가 잇따라 발생하면서 경찰은 본격 수사에 착수했다.\n\n21일 하동경찰서 등에 따르면 17일 오후 12시3분경 하동군 북천면 소재 하동 레일바이크 전용 철로에서 4명이 탑승한 레일바이크 1대가 앞서가던 견인차량을 추돌했다.\n\n이 사고로 70대 탑승자가 크게 다쳐 병원으로 병원으로 이송돼 치료받았으나 다음 날 끝내 숨졌다.\n\n함께 타고 있던 70대 탑승자 2명과 60대 탑승자 1명도 부상을 입고 이송돼 입원 치료를 받고 있다.\n\n앞서 하동 레일바이크는 2일에도 사고가 발생한 바 있다. 당시 해당 구간에서 앞서가던 탑승객이 모자를 줍기 위해 레일바이크를 급정거하며 뒤따르던 6대와 관광용 풍경열차 등이 연쇄 추돌해 16명이 다쳤다.\n\n경찰은 2일 발생한 사고와 이번 사고에 대해 중대재해 여부를 포함해 수사를 확대하고 있다. 아울러 레일바이크 운영 전반의 안전관리 실태 등에 대해서도 조사 중이다.", + "htmlBody": "
\"뉴시스\"

뉴시스 경남 하동에서 레일바이크를 타던 70대 이용객이 숨졌다. 보름 사이 두 차례 추돌사고가 잇따라 발생하면서 경찰은 본격 수사에 착수했다.

21일 하동경찰서 등에 따르면 17일 오후 12시3분경 하동군 북천면 소재 하동 레일바이크 전용 철로에서 4명이 탑승한 레일바이크 1대가 앞서가던 견인차량을 추돌했다.

이 사고로 70대 탑승자가 크게 다쳐 병원으로 병원으로 이송돼 치료받았으나 다음 날 끝내 숨졌다.

함께 타고 있던 70대 탑승자 2명과 60대 탑승자 1명도 부상을 입고 이송돼 입원 치료를 받고 있다.

앞서 하동 레일바이크는 2일에도 사고가 발생한 바 있다. 당시 해당 구간에서 앞서가던 탑승객이 모자를 줍기 위해 레일바이크를 급정거하며 뒤따르던 6대와 관광용 풍경열차 등이 연쇄 추돌해 16명이 다쳤다.

경찰은 2일 발생한 사고와 이번 사고에 대해 중대재해 여부를 포함해 수사를 확대하고 있다. 아울러 레일바이크 운영 전반의 안전관리 실태 등에 대해서도 조사 중이다.

", + "tags": [ + "네이버뉴스", + "사회" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:48:14.000Z" + }, + "createdAt": "2026-05-26T00:20:47.034Z", + "updatedAt": "2026-05-26T00:20:47.034Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-ce255485feca9591", + "title": "스페이스X IPO 앞두고 재무·지배구조 공개… 머스크 의결권 85%", + "sourceLabel": "조선비즈", + "url": "https://n.news.naver.com/mnews/article/366/0001166139", + "lead": "일론 머스크 스페이스X 창업자와 스페이스X 로고./연합뉴스 일론 머스크가 이끄는 항공우주 기업 스페이스X가 기업공개(IPO)를 앞두고 재무 현황과 지배구조, 향후 사업 계획을 공개했다.", + "body": "일론 머스크 스페이스X 창업자와 스페이스X 로고./연합뉴스 일론 머스크가 이끄는 항공우주 기업 스페이스X가 기업공개(IPO)를 앞두고 재무 현황과 지배구조, 향후 사업 계획을 공개했다.\n\n로이터통신 등에 따르면 스페이스X는 20일(현지 시각) 미국 증권거래위원회(SEC)에 투자설명서를 제출하고 본격적인 상장 절차에 들어갔다.\n\n상장 일정은 다음 달 초 투자자 대상 로드쇼를 시작으로, 이르면 같은 달 중순 상장이 이뤄질 가능성이 거론된다. 스페이스X가 상장에 성공할 경우 기업가치는 약 1조7500억달러에 이를 것으로 예상된다.\n\n투자설명서에 따르면 스페이스X는 차등의결권 구조를 채택한다. 일반 투자자에게 판매되는 클래스A 주식에는 주당 1개의 의결권이 부여된다. 반면 머스크 최고경영자(CEO)와 일부 내부자가 보유하는 클래스B 주식에는 주당 10개의 의결권이 주어진다.\n\n이에 따라 머스크 CEO는 전체 의결권의 85.1%를 보유하게 된다. 회사는 머스크 본인을 제외하고는 그를 CEO 자리에서 해임할 수 없도록 하는 구조도 마련했다. 주주가 법적 문제를 제기할 경우 중재 절차를 거치도록 하고, 소송 제기 장소에도 제한을 둔 것으로 전해졌다.\n\n스페이스X는 이번 IPO에서 개인 투자자 참여도 확대할 계획이다. 로이터는 회사 주식의 상당 부분이 개인 투자자에게 배정되며, 다음 달 투자 설명회에 개인 투자자 약 1500명이 초청될 예정이라고 보도했다.\n\n재무 현황도 함께 공개됐다. 스페이스X는 올해 1분기 매출 46억9400만달러를 기록했다. 이 가운데 스타링크 등 위성통신 부문 매출이 32억5700만달러로 가장 큰 비중을 차지했다. 인공지능(AI) 관련 매출은 8억1800만달러, 우주 사업 부문 매출은 6억1900만달러였다. 다만 같은 기간 영업손실은 19억4300만달러로 집계됐다.\n\n스페이스X는 향후 사업 목표로 소행성 채굴, 달과 화성에서의 에너지 생산, 행성 간 이동 등을 제시했다. 또 화성에 영구 기지를 구축하고 100TW(테라와트) 규모의 우주 데이터센터를 건설하는 장기 구상도 담았다. 회사가 제시한 대규모 장기 목표를 달성할 경우에만 머스크가 상당한 금전적 보상을 받을 수 있도록 설계됐다.", + "htmlBody": "
\"일론

일론 머스크 스페이스X 창업자와 스페이스X 로고./연합뉴스 일론 머스크가 이끄는 항공우주 기업 스페이스X가 기업공개(IPO)를 앞두고 재무 현황과 지배구조, 향후 사업 계획을 공개했다.

로이터통신 등에 따르면 스페이스X는 20일(현지 시각) 미국 증권거래위원회(SEC)에 투자설명서를 제출하고 본격적인 상장 절차에 들어갔다.

상장 일정은 다음 달 초 투자자 대상 로드쇼를 시작으로, 이르면 같은 달 중순 상장이 이뤄질 가능성이 거론된다. 스페이스X가 상장에 성공할 경우 기업가치는 약 1조7500억달러에 이를 것으로 예상된다.

투자설명서에 따르면 스페이스X는 차등의결권 구조를 채택한다. 일반 투자자에게 판매되는 클래스A 주식에는 주당 1개의 의결권이 부여된다. 반면 머스크 최고경영자(CEO)와 일부 내부자가 보유하는 클래스B 주식에는 주당 10개의 의결권이 주어진다.

이에 따라 머스크 CEO는 전체 의결권의 85.1%를 보유하게 된다. 회사는 머스크 본인을 제외하고는 그를 CEO 자리에서 해임할 수 없도록 하는 구조도 마련했다. 주주가 법적 문제를 제기할 경우 중재 절차를 거치도록 하고, 소송 제기 장소에도 제한을 둔 것으로 전해졌다.

스페이스X는 이번 IPO에서 개인 투자자 참여도 확대할 계획이다. 로이터는 회사 주식의 상당 부분이 개인 투자자에게 배정되며, 다음 달 투자 설명회에 개인 투자자 약 1500명이 초청될 예정이라고 보도했다.

재무 현황도 함께 공개됐다. 스페이스X는 올해 1분기 매출 46억9400만달러를 기록했다. 이 가운데 스타링크 등 위성통신 부문 매출이 32억5700만달러로 가장 큰 비중을 차지했다. 인공지능(AI) 관련 매출은 8억1800만달러, 우주 사업 부문 매출은 6억1900만달러였다. 다만 같은 기간 영업손실은 19억4300만달러로 집계됐다.

스페이스X는 향후 사업 목표로 소행성 채굴, 달과 화성에서의 에너지 생산, 행성 간 이동 등을 제시했다. 또 화성에 영구 기지를 구축하고 100TW(테라와트) 규모의 우주 데이터센터를 건설하는 장기 구상도 담았다. 회사가 제시한 대규모 장기 목표를 달성할 경우에만 머스크가 상당한 금전적 보상을 받을 수 있도록 설계됐다.

", + "tags": [ + "네이버뉴스", + "IT/과학" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:47:10.000Z" + }, + "createdAt": "2026-05-26T00:20:46.432Z", + "updatedAt": "2026-05-26T00:20:46.432Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-2910deeff66daf76", + "title": "상미당홀딩스, 장애 어린이·가족 돕는다…", + "sourceLabel": "뉴시스", + "url": "https://n.news.naver.com/mnews/article/003/0013959385", + "lead": "임직원 자발적 기부에 회사가 일정 금액 매칭 2012년부터 누적 28억원 돌파…1800명 지원", + "body": "임직원 자발적 기부에 회사가 일정 금액 매칭 2012년부터 누적 28억원 돌파…1800명 지원\n\n20일 서울 서초구 양재동 상미당홀딩스 사옥에서 진행된 '상미당 행복한펀드' 전달식에서 백승훈 행복한재단 사무국장(왼쪽에서 세 번째), 백경학 푸르메재단 상임대표(왼쪽에서 네 번째), 후원 장애 어린이·청소년과 가족들이 기념촬영을 하고 있다. (사진=상미당홀딩스 제공) *재판매 및 DB 금지\n\n[서울=뉴시스]이주혜 기자 = 상미당홀딩스는 장애 어린이·청소년을 위해 임직원 기부로 마련한 '상미당 행복한펀드' 2억원을 푸르메재단에 기부했다고 21일 밝혔다.\n\n'상미당 행복한펀드'는 상미당홀딩스 및 계열사 임직원이 매달 급여의 일부를 자발적으로 기부하면, 회사가 추가로 일정 금액을 출연하는 매칭펀드 방식의 사회공헌 사업이다.\n\n행복한펀드로 조성한 기금은 경제적 어려움이 있는 장애 어린이와 가족들을 위해 사용된다.\n\n푸르메재단을 통해 장애 어린이의 재활 치료와 보조기구 마련을 지원한다.\n\n또 판소리·미술·탁구 등 다양한 예체능 분야에서 재능을 보이는 아이들을 위해 특기·적성 교육과 제주 가족여행 등 다양한 프로그램을 운영한다.\n\n'행복한펀드'는 2012년 시작해 현재까지 누적 28억4000만원이 기부됐다. 약 1800명의 장애 어린이와 가족들이 도움을 받았다.\n\n상미당홀딩스 관계자는 \"행복한펀드는 임직원들의 자발적인 참여와 마음이 모여 만들어진 사회공헌 활동이라는 점에서 더욱 뜻깊다\"며 \"앞으로도 장애 어린이들이 건강하게 성장하고 자신의 꿈을 이어갈 수 있도록 지속적인 나눔 활동을 이어가겠다\"고 말했다.", + "htmlBody": "

임직원 자발적 기부에 회사가 일정 금액 매칭 2012년부터 누적 28억원 돌파…1800명 지원

\"20일

20일 서울 서초구 양재동 상미당홀딩스 사옥에서 진행된 '상미당 행복한펀드' 전달식에서 백승훈 행복한재단 사무국장(왼쪽에서 세 번째), 백경학 푸르메재단 상임대표(왼쪽에서 네 번째), 후원 장애 어린이·청소년과 가족들이 기념촬영을 하고 있다. (사진=상미당홀딩스 제공) *재판매 및 DB 금지

[서울=뉴시스]이주혜 기자 = 상미당홀딩스는 장애 어린이·청소년을 위해 임직원 기부로 마련한 '상미당 행복한펀드' 2억원을 푸르메재단에 기부했다고 21일 밝혔다.

'상미당 행복한펀드'는 상미당홀딩스 및 계열사 임직원이 매달 급여의 일부를 자발적으로 기부하면, 회사가 추가로 일정 금액을 출연하는 매칭펀드 방식의 사회공헌 사업이다.

행복한펀드로 조성한 기금은 경제적 어려움이 있는 장애 어린이와 가족들을 위해 사용된다.

푸르메재단을 통해 장애 어린이의 재활 치료와 보조기구 마련을 지원한다.

또 판소리·미술·탁구 등 다양한 예체능 분야에서 재능을 보이는 아이들을 위해 특기·적성 교육과 제주 가족여행 등 다양한 프로그램을 운영한다.

'행복한펀드'는 2012년 시작해 현재까지 누적 28억4000만원이 기부됐다. 약 1800명의 장애 어린이와 가족들이 도움을 받았다.

상미당홀딩스 관계자는 "행복한펀드는 임직원들의 자발적인 참여와 마음이 모여 만들어진 사회공헌 활동이라는 점에서 더욱 뜻깊다"며 "앞으로도 장애 어린이들이 건강하게 성장하고 자신의 꿈을 이어갈 수 있도록 지속적인 나눔 활동을 이어가겠다"고 말했다.

", + "tags": [ + "네이버뉴스", + "생활/문화" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:38:14.000Z" + }, + "createdAt": "2026-05-26T00:20:45.885Z", + "updatedAt": "2026-05-26T00:20:45.885Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-62ef5942ae8fe865", + "title": "청소년 40년 만에 절반 가까이 줄어", + "sourceLabel": "국제신문", + "url": "https://n.news.naver.com/mnews/article/658/0000144549", + "lead": "‘2026 청소년 통계’ 인포그래픽. 성평등가족부 제공 청소년 인구(9∼24세)가 40년 만에 거의 절반으로 줄었다.", + "body": "‘2026 청소년 통계’ 인포그래픽. 성평등가족부 제공 청소년 인구(9∼24세)가 40년 만에 거의 절반으로 줄었다.\n\n21일 성평등가족부 ‘2026 청소년 통계’에 따르면 올해 청소년 인구는 740만9000명으로 집계됐다. 작년(762만6000명)과 비교하면 2.8%, 40년 전(1385만3000명)보다 46.5% 감소한 수치다.\n\n청소년 인구가 전체 인구에서 차지하는 비율은 1986년 33.6%에서 작년 14.8%로 줄었다. 올해도 14.4%에 그쳤다.\n\n청소년 인구가 줄면서 학령인구(6∼21세)도 감소했다. 올해 학령인구는 678만5000명으로 작년(697만8000명) 대비 2.8% 감소했다. 총인구 대비 비중은 13.5%에서 13.1%로 0.4%p 감소했다.\n\n이런 추세는 앞으로도 계속될 것으로 보인다. 2070년 청소년 인구는 325만7000명으로 전체 인구 대비 8.8%, 학령인구는 290만9000명으로 전체 인구의 7.8%를 차지하게 될 것으로 추정된다.\n\n다문화 학생은 증가세를 유지하고 있다. 작년 기준 다문화 초중고교생은 20만2208명으로 2015년(8만2536명)보다 145.0%, 2020년(14만7378명)보다 37.2% 증가했다. 전체 학생 중 구성비도 같은 기간 2015년 1.4%에서 2020년 2.8%, 작년 4.0%로 꾸준히 늘었다.\n\n청소년 통계는 성평등부가 매년 5월 청소년의 달을 맞아 한국청소년정책연구원과 함께 청소년의 삶을 다각적으로 조명하고 변화를 파악하기 위해 작성해 왔다.", + "htmlBody": "

올해 741만 명…40년 전보다 47%↓

\"‘2026

‘2026 청소년 통계’ 인포그래픽. 성평등가족부 제공 청소년 인구(9∼24세)가 40년 만에 거의 절반으로 줄었다.

21일 성평등가족부 ‘2026 청소년 통계’에 따르면 올해 청소년 인구는 740만9000명으로 집계됐다. 작년(762만6000명)과 비교하면 2.8%, 40년 전(1385만3000명)보다 46.5% 감소한 수치다.

청소년 인구가 전체 인구에서 차지하는 비율은 1986년 33.6%에서 작년 14.8%로 줄었다. 올해도 14.4%에 그쳤다.

청소년 인구가 줄면서 학령인구(6∼21세)도 감소했다. 올해 학령인구는 678만5000명으로 작년(697만8000명) 대비 2.8% 감소했다. 총인구 대비 비중은 13.5%에서 13.1%로 0.4%p 감소했다.

이런 추세는 앞으로도 계속될 것으로 보인다. 2070년 청소년 인구는 325만7000명으로 전체 인구 대비 8.8%, 학령인구는 290만9000명으로 전체 인구의 7.8%를 차지하게 될 것으로 추정된다.

다문화 학생은 증가세를 유지하고 있다. 작년 기준 다문화 초중고교생은 20만2208명으로 2015년(8만2536명)보다 145.0%, 2020년(14만7378명)보다 37.2% 증가했다. 전체 학생 중 구성비도 같은 기간 2015년 1.4%에서 2020년 2.8%, 작년 4.0%로 꾸준히 늘었다.

청소년 통계는 성평등부가 매년 5월 청소년의 달을 맞아 한국청소년정책연구원과 함께 청소년의 삶을 다각적으로 조명하고 변화를 파악하기 위해 작성해 왔다.

", + "tags": [ + "네이버뉴스", + "사회" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T06:22:20.000Z" + }, + "createdAt": "2026-05-26T00:20:45.137Z", + "updatedAt": "2026-05-26T00:20:45.137Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-57c3259d60fd26a3", + "title": "김찬술 후보 \"대덕 잠재력 깨운다\" 순환경제 모델 제시", + "sourceLabel": "대전일보", + "url": "https://n.news.naver.com/mnews/article/656/0000176933", + "lead": "공식 선거운동 첫날 출정식 갖고 본선 행보 고향사랑기부제·반값여행으로 생활인구 확대 계족산·대청호 관광경제 지역상권과 연결", + "body": "공식 선거운동 첫날 출정식 갖고 본선 행보 고향사랑기부제·반값여행으로 생활인구 확대 계족산·대청호 관광경제 지역상권과 연결\n\n21일 6·3 지방선거 공식 선거운동이 시작된 가운데 더불어민주당 김찬술 대전 대덕구청장 후보(왼쪽)가 대덕구 중리동 하나로네거리에서 출정식을 열고 유권자들에게 지지를 호소하고 있다. 김찬술 후보 제공\n\n6·3 지방선거 공식 선거운동이 시작된 가운데 더불어민주당 김찬술 대전 대덕구청장 후보는 출정식을 열고 본선 행보를 시작하며 고향사랑기부제와 지역화폐, 생태관광을 연계한 대덕형 순환경제 모델을 전면에 내세웠다.\n\n김 후보는 21일 대덕구 중리동 하나로네거리에서 출정식을 열고 공식 선거운동에 돌입했다. 출정식에는 박정현 의원(대전 대덕구)을 비롯해 광역·기초의원 후보, 당원과 지지자 등 200여 명이 참석했다.\n\n박 의원은 \"이제는 대덕의 잠재력을 움직이고 구민이 체감할 수 있는 변화를 만들어야 할 시점\"이라며 \"김 후보는 현장을 가장 잘 알고 멈춰 있던 대덕을 다시 움직일 적임자\"라고 지지를 호소했다.\n\n김 후보는 \"정체된 산업과 도시의 흐름을 다시 살리고 구민이 체감하는 변화로 대덕을 확실히 바꾸겠다\"며 \"정쟁보다 민생, 말보다 실행으로 답하겠다\"고 밝혔다.\n\n민생경제 공약의 한 축으로는 대덕형 순환경제 모델을 제시했다. 김 후보는 최근 선거사무소에서 지속가능관광포럼이 마련한 지역 곳간을 키우는 순환형 지역경제 실천 서약에 참여하고 고향사랑기부제와 지역화폐, 생태관광을 묶은 지역경제 선순환 방안을 내놨다.\n\n서약에는 고향사랑기부제 활성화와 반값여행을 통한 생활인구 확대, 지역화폐 기반 소비 촉진, 사이버주민증을 활용한 복수주소 시대 준비, 지속가능관광 추진체계 마련 등이 담겼다.\n\n김 후보는 고향사랑기부와 지역화폐를 민생경제 회복의 연결축으로 삼겠다는 계획이다. 기부금은 지역 재원으로 축적하고 관광객과 생활인구의 소비는 지역화폐를 통해 상권으로 유도해 경제 효과가 대덕 안에서 돌도록 하겠다는 구상이다.\n\n지역화폐 활용 범위를 넓히는 방안도 함께 제안했다. 지역상권과 전통시장, 도매시장 등에서 지역화폐가 실질적으로 쓰일 수 있도록 업종과 소비 특성을 반영한 차별화된 인센티브 체계를 마련하겠다는 것이다.\n\n대청호와 계족산을 중심으로 한 체류형 생태관광 전략도 포함됐다. 자연 자원을 관광객이 머물며 소비하는 기반으로 키워 지역상권과 주민 소득으로 이어지게 하겠다는 계획이다.", + "htmlBody": "

공식 선거운동 첫날 출정식 갖고 본선 행보 고향사랑기부제·반값여행으로 생활인구 확대 계족산·대청호 관광경제 지역상권과 연결

\"21일

21일 6·3 지방선거 공식 선거운동이 시작된 가운데 더불어민주당 김찬술 대전 대덕구청장 후보(왼쪽)가 대덕구 중리동 하나로네거리에서 출정식을 열고 유권자들에게 지지를 호소하고 있다. 김찬술 후보 제공

6·3 지방선거 공식 선거운동이 시작된 가운데 더불어민주당 김찬술 대전 대덕구청장 후보는 출정식을 열고 본선 행보를 시작하며 고향사랑기부제와 지역화폐, 생태관광을 연계한 대덕형 순환경제 모델을 전면에 내세웠다.

김 후보는 21일 대덕구 중리동 하나로네거리에서 출정식을 열고 공식 선거운동에 돌입했다. 출정식에는 박정현 의원(대전 대덕구)을 비롯해 광역·기초의원 후보, 당원과 지지자 등 200여 명이 참석했다.

박 의원은 "이제는 대덕의 잠재력을 움직이고 구민이 체감할 수 있는 변화를 만들어야 할 시점"이라며 "김 후보는 현장을 가장 잘 알고 멈춰 있던 대덕을 다시 움직일 적임자"라고 지지를 호소했다.

김 후보는 "정체된 산업과 도시의 흐름을 다시 살리고 구민이 체감하는 변화로 대덕을 확실히 바꾸겠다"며 "정쟁보다 민생, 말보다 실행으로 답하겠다"고 밝혔다.

민생경제 공약의 한 축으로는 대덕형 순환경제 모델을 제시했다. 김 후보는 최근 선거사무소에서 지속가능관광포럼이 마련한 지역 곳간을 키우는 순환형 지역경제 실천 서약에 참여하고 고향사랑기부제와 지역화폐, 생태관광을 묶은 지역경제 선순환 방안을 내놨다.

서약에는 고향사랑기부제 활성화와 반값여행을 통한 생활인구 확대, 지역화폐 기반 소비 촉진, 사이버주민증을 활용한 복수주소 시대 준비, 지속가능관광 추진체계 마련 등이 담겼다.

김 후보는 고향사랑기부와 지역화폐를 민생경제 회복의 연결축으로 삼겠다는 계획이다. 기부금은 지역 재원으로 축적하고 관광객과 생활인구의 소비는 지역화폐를 통해 상권으로 유도해 경제 효과가 대덕 안에서 돌도록 하겠다는 구상이다.

지역화폐 활용 범위를 넓히는 방안도 함께 제안했다. 지역상권과 전통시장, 도매시장 등에서 지역화폐가 실질적으로 쓰일 수 있도록 업종과 소비 특성을 반영한 차별화된 인센티브 체계를 마련하겠다는 것이다.

대청호와 계족산을 중심으로 한 체류형 생태관광 전략도 포함됐다. 자연 자원을 관광객이 머물며 소비하는 기반으로 키워 지역상권과 주민 소득으로 이어지게 하겠다는 계획이다.

", + "tags": [ + "네이버뉴스", + "정치" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T06:21:15.000Z" + }, + "createdAt": "2026-05-26T00:20:44.422Z", + "updatedAt": "2026-05-26T00:20:44.422Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-220fc9d748b9a36d", + "title": "김석준 부산교육감 후보 첫 유세", + "sourceLabel": "뉴시스", + "url": "https://n.news.naver.com/mnews/article/003/0013958541", + "lead": "[부산=뉴시스] 하경민 기자 = 제9회 전국동시지방선거 공식 선거운동 첫날인 21일 부산 부산진구 서면교차로에서 김석준 부산교육감 후보가 출근길 시민들에게 인사하며 지지를 호소하고 있다. 2026.05.21. yulnetphoto@newsis.com", + "body": "[부산=뉴시스] 하경민 기자 = 제9회 전국동시지방선거 공식 선거운동 첫날인 21일 부산 부산진구 서면교차로에서 김석준 부산교육감 후보가 출근길 시민들에게 인사하며 지지를 호소하고 있다. 2026.05.21. yulnetphoto@newsis.com", + "htmlBody": "
\"기사

[부산=뉴시스] 하경민 기자 = 제9회 전국동시지방선거 공식 선거운동 첫날인 21일 부산 부산진구 서면교차로에서 김석준 부산교육감 후보가 출근길 시민들에게 인사하며 지지를 호소하고 있다. 2026.05.21. yulnetphoto@newsis.com

", + "tags": [ + "네이버뉴스", + "정치" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-20T23:32:50.000Z" + }, + "createdAt": "2026-05-26T00:13:00.511Z", + "updatedAt": "2026-05-26T00:13:00.511Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-cbaef5fcc8a0ce18", + "title": "LH, 부천중동 선도지구 반달마을A 업무협약 체결", + "sourceLabel": "이데일리", + "url": "https://n.news.naver.com/mnews/article/018/0006286583", + "lead": "연내 특별정비구역 지정 목표 [이데일리 최정희 기자] 한국토지주택공사(LH)는 20일 부천중동 노후계획도시 선도지구인 반달마을A구역 통합재건축 주민대표단과 특별정비구역 지정 추진을 위한 업무협약을 체결했다고 밝혔다.", + "body": "연내 특별정비구역 지정 목표 [이데일리 최정희 기자] 한국토지주택공사(LH)는 20일 부천중동 노후계획도시 선도지구인 반달마을A구역 통합재건축 주민대표단과 특별정비구역 지정 추진을 위한 업무협약을 체결했다고 밝혔다.\n\n강오순 한국토지주택공사(LH) 지역균형본부장(오른쪽)과 구동림 반달마을A구역 주민대표단 위원장(왼쪽)이 20일 LH 경기남부지역본부에서 협약 기념사진을 촬영하고 있다.\n\n노후계획도시 정비사업은 조성 후 20년 이상 지난 택지지구 가운데 100만㎡ 이상 규모 지역 등을 대상으로 추진된다. 기존 단일 단지 중심 재건축 방식이 아닌 인접 단지와 기반시설을 포함한 통합정비를 통해 정주 여건을 개선하는 사업이다.\n\n부천중동 1기 신도시에는 현재 총 18개의 특별정비예정구역이 지정돼 있다. 이 가운데 은하마을과 반달마을A가 선도지구로 선정된 상태다.\n\n이번 협약은 반달마을A구역의 예비사업시행자인 LH가 특별정비구역 지정 과정에서 주민 의견을 적극 반영하고 안정적인 통합재건축 사업 추진을 위한 협업 체계를 구축하기 위해 마련됐다.\n\n협약에 따라 주민대표단은 사업 추진 과정에서 주민 의사결정과 동의서 확보 등을 맡는다. LH는 특별정비계획 수립과 인허가 지원, 초기 사업비 투입 등 정비사업 전반을 지원할 예정이다.\n\nLH는 올해 하반기 특별정비계획 지정제안서 사전자문 신청을 시작으로 연내 특별정비구역 지정을 목표로 관련 절차를 신속히 추진한다는 방침이다.\n\n현재 정비계획 입안 예정안에 따르면 반달마을A구역은 기존 3570가구에서 4429가구 규모의 대단지 아파트로 재탄생할 전망이다.\n\n강오순 LH 지역균형본부장은 “반달마을A구역은 중동 노후계획도시 정비사업의 중요한 선도 모델이 될 수 있는 지역”이라며 “공공의 전문성과 지원 역량을 바탕으로 사업이 안정적이고 속도감 있게 추진될 수 있도록 적극 지원하겠다”고 말했다.", + "htmlBody": "

연내 특별정비구역 지정 목표 [이데일리 최정희 기자] 한국토지주택공사(LH)는 20일 부천중동 노후계획도시 선도지구인 반달마을A구역 통합재건축 주민대표단과 특별정비구역 지정 추진을 위한 업무협약을 체결했다고 밝혔다.

\"강오순
강오순 한국토지주택공사(LH) 지역균형본부장(오른쪽)과 구동림 반달마을A구역 주민대표단 위원장(왼쪽)이 20일 LH 경기남부지역본부에서 협약 기념사진을 촬영하고 있다.

노후계획도시 정비사업은 조성 후 20년 이상 지난 택지지구 가운데 100만㎡ 이상 규모 지역 등을 대상으로 추진된다. 기존 단일 단지 중심 재건축 방식이 아닌 인접 단지와 기반시설을 포함한 통합정비를 통해 정주 여건을 개선하는 사업이다.

부천중동 1기 신도시에는 현재 총 18개의 특별정비예정구역이 지정돼 있다. 이 가운데 은하마을과 반달마을A가 선도지구로 선정된 상태다.

이번 협약은 반달마을A구역의 예비사업시행자인 LH가 특별정비구역 지정 과정에서 주민 의견을 적극 반영하고 안정적인 통합재건축 사업 추진을 위한 협업 체계를 구축하기 위해 마련됐다.

협약에 따라 주민대표단은 사업 추진 과정에서 주민 의사결정과 동의서 확보 등을 맡는다. LH는 특별정비계획 수립과 인허가 지원, 초기 사업비 투입 등 정비사업 전반을 지원할 예정이다.

LH는 올해 하반기 특별정비계획 지정제안서 사전자문 신청을 시작으로 연내 특별정비구역 지정을 목표로 관련 절차를 신속히 추진한다는 방침이다.

현재 정비계획 입안 예정안에 따르면 반달마을A구역은 기존 3570가구에서 4429가구 규모의 대단지 아파트로 재탄생할 전망이다.

강오순 LH 지역균형본부장은 “반달마을A구역은 중동 노후계획도시 정비사업의 중요한 선도 모델이 될 수 있는 지역”이라며 “공공의 전문성과 지원 역량을 바탕으로 사업이 안정적이고 속도감 있게 추진될 수 있도록 적극 지원하겠다”고 말했다.

", + "tags": [ + "네이버뉴스", + "경제" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T00:19:09.000Z" + }, + "createdAt": "2026-05-26T00:12:59.927Z", + "updatedAt": "2026-05-26T00:12:59.927Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-803b383dbb3f0ca7", + "title": "애덤 포즌 \"달러 패권 흔들린다…美 신뢰·안보동맹 약화가 통화질서 균열로\"[2026금융포럼]", + "sourceLabel": "아시아경제", + "url": "https://n.news.naver.com/mnews/article/277/0005766235", + "lead": "애덤 포즌 美 PIIE 소장 특별강연 \"달러 당장 대체 안되나 절대적 중심성 약화\" \"원화 국제화, 금융 시장 깊이로 한계\" 미국 달러 패권을 지탱해 온 안보동맹과 금융질서에 균열 조짐이 나타나며 달러 시스템이 흔들릴 수 있다는 애덤 포즌 미국 피터슨국제경제연구소(PIIE) 소장의 경고가 나왔다. 세계 경제 불확실성 확대로 안전자산인 달러 강세가 이어지고 있지만 미국의 보호무역주의와 재정 적자, 정치적 불확실성이 중장기적으로 달러 체제의 신뢰를 약화시킬 수 있다는 분석이다.", + "body": "애덤 포즌 美 PIIE 소장 특별강연 \"달러 당장 대체 안되나 절대적 중심성 약화\" \"원화 국제화, 금융 시장 깊이로 한계\" 미국 달러 패권을 지탱해 온 안보동맹과 금융질서에 균열 조짐이 나타나며 달러 시스템이 흔들릴 수 있다는 애덤 포즌 미국 피터슨국제경제연구소(PIIE) 소장의 경고가 나왔다. 세계 경제 불확실성 확대로 안전자산인 달러 강세가 이어지고 있지만 미국의 보호무역주의와 재정 적자, 정치적 불확실성이 중장기적으로 달러 체제의 신뢰를 약화시킬 수 있다는 분석이다.\n\n애덤 포즌 미국 피터슨국제경제연구소(PIIE) 소장이 21일 서울 중구 조선호텔에서 아시아경제 주최로 열린 '2026 아시아금융포럼'에서 줌(Zoom) 연결을 통해 특별강연을 하고 있다. 2026.5.21 김현민 기자\n\n포즌 소장은 21일 서울 중구 웨스틴 조선 서울에서 '미래금융 대전환 : 생산적 자본의 시대와 새로운 금융질서'를 주제로 열린 2026 아시아금융포럼(Asian Financial Forum 2026) 특별강연에서 \"달러 중심 질서는 당분간 유지되겠지만 이전과 같은 절대적 지위를 장담하기 어려워지고 있다\"며 이같이 진단했다. 이날 포즌 소장은 '달러 패권은 지속될 수 있는가 : 글로벌 통화 질서 변화 속 한국의 전략적 선택'을 주제로 발표했다.\n\n포즌 소장은 최근의 '킹달러' 현상이 구조적 달러 강세를 의미하지는 않는다고 진단했다. 그는 \"중동 리스크와 지정학적 갈등으로 안전자산 선호 현상이 강화되면서 단기적으로 달러 유입이 이어지고 있지만 미국 내부에서는 달러 신뢰를 훼손하는 요인들이 누적되고 있다\"며 \"미국의 인플레이션과 재정적자 확대 위험은 향후 달러 가치 하락 압력으로 이어질 가능성이 있다\"고 짚었다.\n\n\"달러는 안보 동맹의 산물\"…美 신뢰 약화가 통화질서 균열로\n\n특히 도널드 트럼프 행정부 출범 이후 강화된 '아메리카 퍼스트' 기조가 달러 중심 국제금융 질서의 신뢰를 약화시키고 있다고 분석했다. 포즌 소장은 \"관세는 한 번 도입되면 거의 철회되지 않는다\"며 \"미국은 전략산업 보호를 이유로 고율 관세와 자국 중심 공급망 정책을 지속할 가능성이 높고, 기존 자유무역 체제로 돌아가기 어려울 것\"이라고 내다봤다. 그러면서 \"동맹국들에 대해서도 투자 확대와 무기 구매, 에너지 구매를 지속 요구할 수 있다\"고 덧붙였다.\n\n포즌 소장은 달러 패권의 핵심 기반이 미국 중심의 안보 체제였다는 점도 강조했다. 그는 \"과거에는 미국이 세계 안보 질서를 주도하면서 동맹국들이 자연스럽게 달러를 외환보유고와 결제 수단으로 선택했다\"며 \"달러는 단순한 통화가 아니라 안보 관계 위에 구축된 시스템\"이라고 말했다. 이어 \"그러나 미국이 동맹 관계를 거래적으로 접근하고 국제질서의 예측 가능성을 약화시키면서 달러 체제 역시 흔들릴 수 있다\"고 덧붙였다.\n\n미국이 과거 글로벌 경제에 제공해 온 '보험 기능' 자체가 약화되고 있다는 점도 언급했다. 포즌 소장은 \"미국은 과거 규칙과 안보를 제공하는 보험자 역할을 했지만 점점 더 강압적이고 거래적인 방식으로 움직이고 있다\"고 지적하며 유럽과 일본, 한국 등의 국방비 지출 확대와 함께 미국의 제재와 감시를 우회하려는 대체 결제 시스템, 통화 스와프라인 확대 움직임도 나타나고 있다고 설명했다.\n\n미국의 재정 및 통화정책도 우려 요인으로 꼽았다. 포즌 소장은 관세와 이민 제한, 국방비 증가, 완화적 재정 정책 등으로 미국의 물가 상승 압력이 예상보다 오래 지속될 가능성이 높다고 전망했다. 특히 국방비 확대와 산업정책, 인공지능(AI) 투자, 지정학적 리스크 증가로 경기를 과열도, 침체도 시키지 않는 중립금리 수준 자체가 높아졌다고 봤다. 그는 \"미국 연방준비제도(Fed)는 현재 통화정책을 충분히 긴축적이라고 판단하고 있지만 실제 금융환경은 생각보다 느슨할 수 있다\"고 지적했다.\n\n애덤 포즌 미국 피터슨국제경제연구소(PIIE) 소장이 21일 서울 중구 조선호텔에서 아시아경제 주최로 열린 '2026 아시아금융포럼'에서 줌(Zoom) 연결을 통해 특별강연을 하고 있다. 2026.5.21 김현민 기자 \"달러 당장 대체 어렵지만 중심성 약화\"…한국엔 전략적 기회\n\n다만 달러를 즉각 대체할 통화가 등장할 가능성은 낮다고 전망했다. 포즌 소장은 유로화와 위안화가 일부 역할을 확대할 수는 있지만 달러 중심 체제가 단기간에 붕괴하기보다는 단계적으로 약화하며 다극화 체제로 이동할 가능성이 크다고 분석했다. 그는 \"달러 수요가 갑자기 사라지지는 않겠지만 이전보다 중심성이 약해질 수 있다\"며 \"지금까지는 위기 발생 시 달러로 자금이 몰렸지만 앞으로는 미국 자체가 불안 요인이 되면 오히려 자금이 빠져나갈 가능성도 있다\"고 경고했다.\n\n포즌 소장은 달러 패권이 약화하는 과정에서 한국의 전략적 역할 확대 가능성도 제시했다. 그는 \"미국과 유럽 기업들의 영향력이 세계 시장에서 장기적으로 약화될 가능성이 있다\"며 \"동남아시아와 중동, 개발도상국 시장에서는 미국이나 중국 어느 한쪽에도 과도하게 의존하지 않는 새로운 협력 파트너에 대한 수요가 커질 수 있다\"고 말했다.\n\n이어 \"한국은 미·중 경쟁 구도 속에서 단순히 어느 한쪽에 의존하기보다 독자적인 경제·금융 네트워크를 강화해야 한다\"며 \"중국·일본·아세안(ASEAN) 등 역내 경제 협력과 다자 금융안전망 활용 측면에서도 새로운 기회가 열릴 수 있다\"고 제언했다. 그는 특히 반도체와 방위산업, 첨단 제조업 분야에서는 한국 기업들의 경쟁력이 향후 더 중요해질 수 있다고 평가했다.\n\n포즌 소장은 원화의 국제화 가능성과 관련해서는 구조적 제약이 존재한다고 진단했다. 그는 원화 국제화 가능하냐는 질문에 \"한국은 매우 정교한 투자자와 기업들을 보유하고 있지만 싱가포르나 런던, 뉴욕 같은 글로벌 금융허브 수준의 금융 서비스 산업은 아직 부족하다\"며 \"한국 정부가 장기간 재정 흑자를 유지해온 만큼 원화 표시 국채 시장 규모와 깊이도 제한적\"이라고 설명했다.\n\n이어 \"미국 정부 특히 트럼프 행정부가 원화 국제화를 달가워하지 않을 가능성도 있다\"고 덧붙였다. 다만 그는 \"원화가 달러나 유로를 대체하는 수준은 아니더라도 싱가포르달러나 노르웨이 크로네처럼 특정 분야와 지역에서 의미 있는 국제 통화 역할을 할 가능성은 있다\"고 평가했다.", + "htmlBody": "

애덤 포즌 美 PIIE 소장 특별강연 "달러 당장 대체 안되나 절대적 중심성 약화" "원화 국제화, 금융 시장 깊이로 한계" 미국 달러 패권을 지탱해 온 안보동맹과 금융질서에 균열 조짐이 나타나며 달러 시스템이 흔들릴 수 있다는 애덤 포즌 미국 피터슨국제경제연구소(PIIE) 소장의 경고가 나왔다. 세계 경제 불확실성 확대로 안전자산인 달러 강세가 이어지고 있지만 미국의 보호무역주의와 재정 적자, 정치적 불확실성이 중장기적으로 달러 체제의 신뢰를 약화시킬 수 있다는 분석이다.

\"애덤

애덤 포즌 미국 피터슨국제경제연구소(PIIE) 소장이 21일 서울 중구 조선호텔에서 아시아경제 주최로 열린 '2026 아시아금융포럼'에서 줌(Zoom) 연결을 통해 특별강연을 하고 있다. 2026.5.21 김현민 기자

포즌 소장은 21일 서울 중구 웨스틴 조선 서울에서 '미래금융 대전환 : 생산적 자본의 시대와 새로운 금융질서'를 주제로 열린 2026 아시아금융포럼(Asian Financial Forum 2026) 특별강연에서 "달러 중심 질서는 당분간 유지되겠지만 이전과 같은 절대적 지위를 장담하기 어려워지고 있다"며 이같이 진단했다. 이날 포즌 소장은 '달러 패권은 지속될 수 있는가 : 글로벌 통화 질서 변화 속 한국의 전략적 선택'을 주제로 발표했다.

포즌 소장은 최근의 '킹달러' 현상이 구조적 달러 강세를 의미하지는 않는다고 진단했다. 그는 "중동 리스크와 지정학적 갈등으로 안전자산 선호 현상이 강화되면서 단기적으로 달러 유입이 이어지고 있지만 미국 내부에서는 달러 신뢰를 훼손하는 요인들이 누적되고 있다"며 "미국의 인플레이션과 재정적자 확대 위험은 향후 달러 가치 하락 압력으로 이어질 가능성이 있다"고 짚었다.

"달러는 안보 동맹의 산물"…美 신뢰 약화가 통화질서 균열로

특히 도널드 트럼프 행정부 출범 이후 강화된 '아메리카 퍼스트' 기조가 달러 중심 국제금융 질서의 신뢰를 약화시키고 있다고 분석했다. 포즌 소장은 "관세는 한 번 도입되면 거의 철회되지 않는다"며 "미국은 전략산업 보호를 이유로 고율 관세와 자국 중심 공급망 정책을 지속할 가능성이 높고, 기존 자유무역 체제로 돌아가기 어려울 것"이라고 내다봤다. 그러면서 "동맹국들에 대해서도 투자 확대와 무기 구매, 에너지 구매를 지속 요구할 수 있다"고 덧붙였다.

포즌 소장은 달러 패권의 핵심 기반이 미국 중심의 안보 체제였다는 점도 강조했다. 그는 "과거에는 미국이 세계 안보 질서를 주도하면서 동맹국들이 자연스럽게 달러를 외환보유고와 결제 수단으로 선택했다"며 "달러는 단순한 통화가 아니라 안보 관계 위에 구축된 시스템"이라고 말했다. 이어 "그러나 미국이 동맹 관계를 거래적으로 접근하고 국제질서의 예측 가능성을 약화시키면서 달러 체제 역시 흔들릴 수 있다"고 덧붙였다.

미국이 과거 글로벌 경제에 제공해 온 '보험 기능' 자체가 약화되고 있다는 점도 언급했다. 포즌 소장은 "미국은 과거 규칙과 안보를 제공하는 보험자 역할을 했지만 점점 더 강압적이고 거래적인 방식으로 움직이고 있다"고 지적하며 유럽과 일본, 한국 등의 국방비 지출 확대와 함께 미국의 제재와 감시를 우회하려는 대체 결제 시스템, 통화 스와프라인 확대 움직임도 나타나고 있다고 설명했다.

미국의 재정 및 통화정책도 우려 요인으로 꼽았다. 포즌 소장은 관세와 이민 제한, 국방비 증가, 완화적 재정 정책 등으로 미국의 물가 상승 압력이 예상보다 오래 지속될 가능성이 높다고 전망했다. 특히 국방비 확대와 산업정책, 인공지능(AI) 투자, 지정학적 리스크 증가로 경기를 과열도, 침체도 시키지 않는 중립금리 수준 자체가 높아졌다고 봤다. 그는 "미국 연방준비제도(Fed)는 현재 통화정책을 충분히 긴축적이라고 판단하고 있지만 실제 금융환경은 생각보다 느슨할 수 있다"고 지적했다.

\"애덤

애덤 포즌 미국 피터슨국제경제연구소(PIIE) 소장이 21일 서울 중구 조선호텔에서 아시아경제 주최로 열린 '2026 아시아금융포럼'에서 줌(Zoom) 연결을 통해 특별강연을 하고 있다. 2026.5.21 김현민 기자 "달러 당장 대체 어렵지만 중심성 약화"…한국엔 전략적 기회

다만 달러를 즉각 대체할 통화가 등장할 가능성은 낮다고 전망했다. 포즌 소장은 유로화와 위안화가 일부 역할을 확대할 수는 있지만 달러 중심 체제가 단기간에 붕괴하기보다는 단계적으로 약화하며 다극화 체제로 이동할 가능성이 크다고 분석했다. 그는 "달러 수요가 갑자기 사라지지는 않겠지만 이전보다 중심성이 약해질 수 있다"며 "지금까지는 위기 발생 시 달러로 자금이 몰렸지만 앞으로는 미국 자체가 불안 요인이 되면 오히려 자금이 빠져나갈 가능성도 있다"고 경고했다.

포즌 소장은 달러 패권이 약화하는 과정에서 한국의 전략적 역할 확대 가능성도 제시했다. 그는 "미국과 유럽 기업들의 영향력이 세계 시장에서 장기적으로 약화될 가능성이 있다"며 "동남아시아와 중동, 개발도상국 시장에서는 미국이나 중국 어느 한쪽에도 과도하게 의존하지 않는 새로운 협력 파트너에 대한 수요가 커질 수 있다"고 말했다.

이어 "한국은 미·중 경쟁 구도 속에서 단순히 어느 한쪽에 의존하기보다 독자적인 경제·금융 네트워크를 강화해야 한다"며 "중국·일본·아세안(ASEAN) 등 역내 경제 협력과 다자 금융안전망 활용 측면에서도 새로운 기회가 열릴 수 있다"고 제언했다. 그는 특히 반도체와 방위산업, 첨단 제조업 분야에서는 한국 기업들의 경쟁력이 향후 더 중요해질 수 있다고 평가했다.

포즌 소장은 원화의 국제화 가능성과 관련해서는 구조적 제약이 존재한다고 진단했다. 그는 원화 국제화 가능하냐는 질문에 "한국은 매우 정교한 투자자와 기업들을 보유하고 있지만 싱가포르나 런던, 뉴욕 같은 글로벌 금융허브 수준의 금융 서비스 산업은 아직 부족하다"며 "한국 정부가 장기간 재정 흑자를 유지해온 만큼 원화 표시 국채 시장 규모와 깊이도 제한적"이라고 설명했다.

이어 "미국 정부 특히 트럼프 행정부가 원화 국제화를 달가워하지 않을 가능성도 있다"고 덧붙였다. 다만 그는 "원화가 달러나 유로를 대체하는 수준은 아니더라도 싱가포르달러나 노르웨이 크로네처럼 특정 분야와 지역에서 의미 있는 국제 통화 역할을 할 가능성은 있다"고 평가했다.

", + "tags": [ + "네이버뉴스", + "경제" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:47:02.000Z" + }, + "createdAt": "2026-05-26T00:12:59.359Z", + "updatedAt": "2026-05-26T00:12:59.359Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-0708fa7166d7dcb8", + "title": "철도공단, ‘GTX 철근 누락’ 기둥 안전성 검증 착수", + "sourceLabel": "헤럴드경제", + "url": "https://n.news.naver.com/mnews/article/016/0002646593", + "lead": "콘크리트학회, 기둥보강 적정성 검토 9월까지 4~5개월간 면밀 검증 예정", + "body": "콘크리트학회, 기둥보강 적정성 검토 9월까지 4~5개월간 면밀 검증 예정\n\n국가철도공단은 철근 누락이 발견된 수도권광역급행철도(GTX) 삼성역 공사와 관련해 철저한 구조물 안전성 검증을 위해 전문 공인기관을 통한 ‘기둥 보강 적정성 검토 용역’에 착수한다고 21일 밝혔다. 영동대로 지하공간 복합개발 3공구 건설공사는 국가철도공단이 서울시에 위탁 시행 중인 사업으로, 시공은 현대건설이 맡고 있다. 지하 5층 승강장 전체 기둥 218개 가운데 80개에서 철근 누락이 확인됐고 이 중 50개는 설계 기준에 미달한 것으로 파악됐다. 누락 규모는 약 178톤에 달한다.\n\n기둥 보강 적정성 검토 용역은 구조물 분야 전문 공인기관인 한국콘크리트학회가 맡아 수행하게 된다. 콘크리트 구조 해석·보강 및 철도 구조물 안전성 평가분야의 학회 중심 전문 연구진을 구성해 이달부터 오는 9월까지 약 4~5개월간 면밀한 검증이 이뤄질 예정이다.\n\n주요 과업 내용은 서울시가 수립한 기둥 보강계획에 대해 ▷삼성역 구조적 성능 검증 ▷보강공법의 안전성 검토 ▷대안 공법 검토 ▷열차 운행과의 연관성 ▷운영 단계의 안전 및 유지관리 등을 종합적으로 검토하는 것이다.\n\n이안호 국가철도공단 이사장 직무대행은 “국민의 안전과 직결된 사안인 만큼, 공신력 있는 전문 학회의 철저하고 객관적인 검증을 거쳐 최적의 보강 방안을 도출할 것”이라며 “과거 유사 보강 사례까지 면밀히 분석해 향후 운영 단계에서도 위험 요소를 최소화해 국민 모두 안전하게 GTX-A를 이용할 수 있도록 최선을 다하겠다”고 밝혔다.\n\n한편 전날 국회 국토교통위원회는 GTX 삼성역 철근 누락 사태와 관련한 긴급 현안질의를 개최했다. 서울시는 GTX-A 삼성역 구간 철근 누락과 관련해 정기보고서를 통해 세 차례 관련 내용을 통보했다고 밝혔으나 철도공단은 서울시가 제출한 건설사업관리보고서 주요 내용 요약에 철근 누락 사항은 반영되지 않았고 본문 시공 실패 사례에서도 ‘해당 사항 없음’으로 돼 있다고 반박해 책임공방이 벌어졌다. 신혜원 기자", + "htmlBody": "

콘크리트학회, 기둥보강 적정성 검토 9월까지 4~5개월간 면밀 검증 예정

국가철도공단은 철근 누락이 발견된 수도권광역급행철도(GTX) 삼성역 공사와 관련해 철저한 구조물 안전성 검증을 위해 전문 공인기관을 통한 ‘기둥 보강 적정성 검토 용역’에 착수한다고 21일 밝혔다. 영동대로 지하공간 복합개발 3공구 건설공사는 국가철도공단이 서울시에 위탁 시행 중인 사업으로, 시공은 현대건설이 맡고 있다. 지하 5층 승강장 전체 기둥 218개 가운데 80개에서 철근 누락이 확인됐고 이 중 50개는 설계 기준에 미달한 것으로 파악됐다. 누락 규모는 약 178톤에 달한다.

기둥 보강 적정성 검토 용역은 구조물 분야 전문 공인기관인 한국콘크리트학회가 맡아 수행하게 된다. 콘크리트 구조 해석·보강 및 철도 구조물 안전성 평가분야의 학회 중심 전문 연구진을 구성해 이달부터 오는 9월까지 약 4~5개월간 면밀한 검증이 이뤄질 예정이다.

주요 과업 내용은 서울시가 수립한 기둥 보강계획에 대해 ▷삼성역 구조적 성능 검증 ▷보강공법의 안전성 검토 ▷대안 공법 검토 ▷열차 운행과의 연관성 ▷운영 단계의 안전 및 유지관리 등을 종합적으로 검토하는 것이다.

이안호 국가철도공단 이사장 직무대행은 “국민의 안전과 직결된 사안인 만큼, 공신력 있는 전문 학회의 철저하고 객관적인 검증을 거쳐 최적의 보강 방안을 도출할 것”이라며 “과거 유사 보강 사례까지 면밀히 분석해 향후 운영 단계에서도 위험 요소를 최소화해 국민 모두 안전하게 GTX-A를 이용할 수 있도록 최선을 다하겠다”고 밝혔다.

한편 전날 국회 국토교통위원회는 GTX 삼성역 철근 누락 사태와 관련한 긴급 현안질의를 개최했다. 서울시는 GTX-A 삼성역 구간 철근 누락과 관련해 정기보고서를 통해 세 차례 관련 내용을 통보했다고 밝혔으나 철도공단은 서울시가 제출한 건설사업관리보고서 주요 내용 요약에 철근 누락 사항은 반영되지 않았고 본문 시공 실패 사례에서도 ‘해당 사항 없음’으로 돼 있다고 반박해 책임공방이 벌어졌다. 신혜원 기자

", + "tags": [ + "네이버뉴스", + "경제" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:47:23.000Z" + }, + "createdAt": "2026-05-26T00:12:58.877Z", + "updatedAt": "2026-05-26T00:12:58.877Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-aa58bbda6f51aa2b", + "title": "[서울&] 각자도생 끝내고 ‘공동체’로 뭉치는 골목상권", + "sourceLabel": "한겨레", + "url": "https://n.news.naver.com/mnews/article/028/0002806087", + "lead": "[한겨레] 이런 조례! 저런 조례! l 골목상권 공동체 육성 및 활성화 지원 조례", + "body": "[한겨레] 이런 조례! 저런 조례! l 골목상권 공동체 육성 및 활성화 지원 조례\n\n중랑구가 지난달 사가정면목로(면목7동)를 제14호 골목형상점가로 신규 지정하고 지정서 수여식을 개최하고 있다. 중랑구 제공\n\n고물가·고금리 장기화의 직격탄을 맞은 동네 골목길 자영업자들의 한숨이 깊어지고 있다. 임대료와 인건비 부담은 치솟는데 소비 심리는 얼어붙으면서 홀로 매장을 지키는 소상공인들의 절박함은 한계에 다다랐다.\n\n서울신용보증재단 정책연구센터가 발간한 보고서에 따르면 현재 전체 자영업자의 20.2%가 경영난 등을 이유로 폐업을 심각하게 고려하고 있는 실정이다. “옆 가게가 망해야 내가 산다”는 식의 삭막한 각자도생 구조 속에서는 더 이상 지속 가능한 생존을 담보하기 어렵다는 목소리가 나오는 이유다.\n\n이제 동네 상권에도 ‘상생과 연대’라는 새로운 생존 방식이 요구되고 있다. 그동안 정부와 지방자치단체의 소상공인 활성화 정책은 주로 대형 전통시장이나 등록 상점가 중심으로 흘러왔다. 여기에는 제도적 ‘보이지않는 벽’이 있었다. 현행 전통시장법상 행정지원을 받거나 온누리상품권 가맹점으로 등록하려면 점포 밀집도나 특정 면적 기준을 까다롭게 충족해야 했기 때문이다. 거대상권과 온라인 플랫폼 사이에서 홀로 분투하던 진짜 동네 골목길이 정책적 사각지대에 방치돼 있었던 셈이다.\n\n최근 5년 사이 이 같은 공백을 메우려는 서울시 자치구 차원의 조례가 만들어져왔다. ‘골목형상점가 활성화 지원 조례’가 대표적이다. ‘전통시장 및 상점가 육성을 위한 특별법’에 근거를 두고 시행령이 위임한 사항을 자치구가 구체화한 조례로, 일정 면적·밀집도를 갖춘 구역을 자치구청장이 ‘골목형상점가’로 지정해 전통시장에 준하는 경영·시설 현대화 지원을 제공한다. 지정된 골목형상점가는 시설 현대화 사업과 경영 현대화 사업 지원을 받을 수 있고 상인이나 상인조직은 온누리상품권 가맹점 등록도 신청할 수 있다.\n\n지난해부터는 대부분의 자치구에서 점포밀집 기준을 잇달아 완화하는 추세다. 2021년부터 해마다 잇달아 조례를 시행한 중구와 용산구, 동대문구는 모두 지난해 기존 점포 밀집 기준을 30개에서 15개로 낮췄다. 중구는 2000㎡당 소상공인 점포 15개 이상이 밀집한 구역을 기준으로 삼고, 지난해 7월에는 일부개정안을 시행해 지정 기준의 면적 산정 방식을 보완했다. 동대문구의 경우 2023년 9월 첫 지정 이후 제기동 고대앞 마을, 신설동 들락거리, 회기역 골목형상점가 등 총 9곳이 운영 중이다. 영등포구·관악구·광진구·중랑구·금천구 등 다수 자치구도 비슷한 조례를 시행하고 있다.\n\n각 자치구에서 움직임이 쌓이면서 국회에서도 2024년 7월 송재봉 의원이 ‘골목상권 공동체 육성 및 활성화 지원을 위한 특별법안’을 대표 발의했다. 법안의 제안 이유에는 광역지자체 조례만으로는 골목상권의 체계적 발전에 한계가 있어 법률 차원의 지원 근거가 필요하다는 취지가 담겨 있다. 지역에서 시작된 제도가 중앙 입법으로 확장되는 흐름인 셈이다.\n\n최근에는 서울신용보증재단의 데이터 행정과 결합하며 실질적인 금융·비금융 시너지를 내고 있다. 재단 정책연구센터가 발간한 ‘서울시 경영지원 사업 효과분석(이슈리포트 25-2호)’에 따르면, 재단의 경영위기 알람 모형을 통해 선제적으로 발굴된 골목상권 소상공인들은 평균 신용등급 5.05등급, 연 매출액 1억5498만원으로 경영 환경이 매우 취약한 상태였다. 그러나 이들이 조례를 통해 상인 구역을 구성하고 재단의 밀착 맞춤형 비금융 컨설팅을 받았을 때, 1년 후 일반 비교군 대비 고금리인 제2금융권 대출잔액이 5.0%포인트나 더 크게 감소하는 ‘대출포트폴리오 개선 효과’가 뚜렷하게 나타났다. 제도적 조직화가 소상공인의 고질적인 고금리 금융 비용 부담을 덜어주는 실질적인 방어막이 된 셈이다.\n\n서울시의 소상공인 관련 조례는 시대의 변화에 발맞춰 꾸준히 진화해왔다. 서울시 차원의 소상공인 관련 입법은 2009년 ‘서울특별시 유통업 상생협력과 소상공인 보호 조례’ 발의에서 출발해, 2014년 ‘서울특별시 소상공인 지원에 관한 조례’가 별도로 제정되면서 본격적인 법적 근거가 마련됐다. 이 조례는 현재 서울신용보증재단이 수행하는 소상공인 종합지원 사업의 추진 근거가 되고 있다.\n\n채소라 객원기자 mylovelypizza@naver.com\n\n서울살이 길라잡이 서울앤(www.seouland.com) 취재팀 편집", + "htmlBody": "

[한겨레] 이런 조례! 저런 조례! l 골목상권 공동체 육성 및 활성화 지원 조례

\"중랑구가

중랑구가 지난달 사가정면목로(면목7동)를 제14호 골목형상점가로 신규 지정하고 지정서 수여식을 개최하고 있다. 중랑구 제공

고물가·고금리 장기화의 직격탄을 맞은 동네 골목길 자영업자들의 한숨이 깊어지고 있다. 임대료와 인건비 부담은 치솟는데 소비 심리는 얼어붙으면서 홀로 매장을 지키는 소상공인들의 절박함은 한계에 다다랐다.

서울신용보증재단 정책연구센터가 발간한 보고서에 따르면 현재 전체 자영업자의 20.2%가 경영난 등을 이유로 폐업을 심각하게 고려하고 있는 실정이다. “옆 가게가 망해야 내가 산다”는 식의 삭막한 각자도생 구조 속에서는 더 이상 지속 가능한 생존을 담보하기 어렵다는 목소리가 나오는 이유다.

이제 동네 상권에도 ‘상생과 연대’라는 새로운 생존 방식이 요구되고 있다. 그동안 정부와 지방자치단체의 소상공인 활성화 정책은 주로 대형 전통시장이나 등록 상점가 중심으로 흘러왔다. 여기에는 제도적 ‘보이지않는 벽’이 있었다. 현행 전통시장법상 행정지원을 받거나 온누리상품권 가맹점으로 등록하려면 점포 밀집도나 특정 면적 기준을 까다롭게 충족해야 했기 때문이다. 거대상권과 온라인 플랫폼 사이에서 홀로 분투하던 진짜 동네 골목길이 정책적 사각지대에 방치돼 있었던 셈이다.

최근 5년 사이 이 같은 공백을 메우려는 서울시 자치구 차원의 조례가 만들어져왔다. ‘골목형상점가 활성화 지원 조례’가 대표적이다. ‘전통시장 및 상점가 육성을 위한 특별법’에 근거를 두고 시행령이 위임한 사항을 자치구가 구체화한 조례로, 일정 면적·밀집도를 갖춘 구역을 자치구청장이 ‘골목형상점가’로 지정해 전통시장에 준하는 경영·시설 현대화 지원을 제공한다. 지정된 골목형상점가는 시설 현대화 사업과 경영 현대화 사업 지원을 받을 수 있고 상인이나 상인조직은 온누리상품권 가맹점 등록도 신청할 수 있다.

지난해부터는 대부분의 자치구에서 점포밀집 기준을 잇달아 완화하는 추세다. 2021년부터 해마다 잇달아 조례를 시행한 중구와 용산구, 동대문구는 모두 지난해 기존 점포 밀집 기준을 30개에서 15개로 낮췄다. 중구는 2000㎡당 소상공인 점포 15개 이상이 밀집한 구역을 기준으로 삼고, 지난해 7월에는 일부개정안을 시행해 지정 기준의 면적 산정 방식을 보완했다. 동대문구의 경우 2023년 9월 첫 지정 이후 제기동 고대앞 마을, 신설동 들락거리, 회기역 골목형상점가 등 총 9곳이 운영 중이다. 영등포구·관악구·광진구·중랑구·금천구 등 다수 자치구도 비슷한 조례를 시행하고 있다.

각 자치구에서 움직임이 쌓이면서 국회에서도 2024년 7월 송재봉 의원이 ‘골목상권 공동체 육성 및 활성화 지원을 위한 특별법안’을 대표 발의했다. 법안의 제안 이유에는 광역지자체 조례만으로는 골목상권의 체계적 발전에 한계가 있어 법률 차원의 지원 근거가 필요하다는 취지가 담겨 있다. 지역에서 시작된 제도가 중앙 입법으로 확장되는 흐름인 셈이다.

최근에는 서울신용보증재단의 데이터 행정과 결합하며 실질적인 금융·비금융 시너지를 내고 있다. 재단 정책연구센터가 발간한 ‘서울시 경영지원 사업 효과분석(이슈리포트 25-2호)’에 따르면, 재단의 경영위기 알람 모형을 통해 선제적으로 발굴된 골목상권 소상공인들은 평균 신용등급 5.05등급, 연 매출액 1억5498만원으로 경영 환경이 매우 취약한 상태였다. 그러나 이들이 조례를 통해 상인 구역을 구성하고 재단의 밀착 맞춤형 비금융 컨설팅을 받았을 때, 1년 후 일반 비교군 대비 고금리인 제2금융권 대출잔액이 5.0%포인트나 더 크게 감소하는 ‘대출포트폴리오 개선 효과’가 뚜렷하게 나타났다. 제도적 조직화가 소상공인의 고질적인 고금리 금융 비용 부담을 덜어주는 실질적인 방어막이 된 셈이다.

서울시의 소상공인 관련 조례는 시대의 변화에 발맞춰 꾸준히 진화해왔다. 서울시 차원의 소상공인 관련 입법은 2009년 ‘서울특별시 유통업 상생협력과 소상공인 보호 조례’ 발의에서 출발해, 2014년 ‘서울특별시 소상공인 지원에 관한 조례’가 별도로 제정되면서 본격적인 법적 근거가 마련됐다. 이 조례는 현재 서울신용보증재단이 수행하는 소상공인 종합지원 사업의 추진 근거가 되고 있다.

채소라 객원기자 mylovelypizza@naver.com

서울살이 길라잡이 서울앤(www.seouland.com) 취재팀 편집

", + "tags": [ + "네이버뉴스", + "생활/문화" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:38:08.000Z" + }, + "createdAt": "2026-05-26T00:12:58.007Z", + "updatedAt": "2026-05-26T00:12:58.007Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-a3d51d1e9adec72b", + "title": "\"운전의 즐거움, 과학적으로 측정한다면\"…폴스타, 옥스퍼드대와 연구", + "sourceLabel": "머니투데이", + "url": "https://n.news.naver.com/mnews/article/008/0005361114", + "lead": "폴스타가 옥스퍼트대 SDG 임팩트 랩과 파일럿 연구에 착수했다./사진=폴스타 스웨덴 전기차 브랜드 폴스타는 옥스퍼드대학교 'SDG 임팩트 랩(Impact Lab)'과 함께 '운전의 즐거움'을 과학적으로 정의·측정하기 위한 파일럿 연구를 시작했다고 21일 밝혔다.", + "body": "폴스타가 옥스퍼트대 SDG 임팩트 랩과 파일럿 연구에 착수했다./사진=폴스타 스웨덴 전기차 브랜드 폴스타는 옥스퍼드대학교 'SDG 임팩트 랩(Impact Lab)'과 함께 '운전의 즐거움'을 과학적으로 정의·측정하기 위한 파일럿 연구를 시작했다고 21일 밝혔다.\n\n전기차 보편화를 고려한 현대적인 주행 경험을 설명하기 위해서는 가속력, 최고속도 등 기존 내연기관 중심의 성능 기준을 넘어선 새로운 접근이 필요하다는 관점에서 이번 연구를 시작했다. 폴스타와 옥스퍼드대 SDG 임팩트 랩은 뇌와 신체에서 측정 가능한 신호를 분석해 운전자가 실제로 느끼는 감각적, 정서적 경험을 이해하고 이를 정량화할 수 있는 새로운 퍼포먼스의 기준을 모색할 계획이다.\n\n이 연구는 공학(Engineering science)과 실험심리학(Experimental psychology) 분야 전문성을 결합해 진행한다. 연구팀은 참가자가 폴스타 차량을 운전하는 동안 나타나는 생리적, 인지적, 행동적 반응을 분석할 예정이다. 특히 뇌 활동, 생체 데이터, 운전 행동 데이터를 정밀 분석해 운전자가 느끼는 흥분감과 즐거움이 실제로 관찰, 분석 및 정량화될 수 있는지 확인할 계획이다.\n\n알렉산더 베츠 옥스퍼드대 부총장 겸 SDG 임팩트 랩 공동 설립자는 \"이번 프로젝트는 학술 연구가 실제 산업과 사회에 어떻게 영향을 미칠 수 있는지를 보여주는 좋은 사례\"라며 \"폴스타와 협업을 통해 과학적 분석과 통찰을 미래의 혁신을 이끌어갈 지식으로 전환할 수 있을 것\"이라고 말했다.", + "htmlBody": "
\"폴스타가

폴스타가 옥스퍼트대 SDG 임팩트 랩과 파일럿 연구에 착수했다./사진=폴스타 스웨덴 전기차 브랜드 폴스타는 옥스퍼드대학교 'SDG 임팩트 랩(Impact Lab)'과 함께 '운전의 즐거움'을 과학적으로 정의·측정하기 위한 파일럿 연구를 시작했다고 21일 밝혔다.

전기차 보편화를 고려한 현대적인 주행 경험을 설명하기 위해서는 가속력, 최고속도 등 기존 내연기관 중심의 성능 기준을 넘어선 새로운 접근이 필요하다는 관점에서 이번 연구를 시작했다. 폴스타와 옥스퍼드대 SDG 임팩트 랩은 뇌와 신체에서 측정 가능한 신호를 분석해 운전자가 실제로 느끼는 감각적, 정서적 경험을 이해하고 이를 정량화할 수 있는 새로운 퍼포먼스의 기준을 모색할 계획이다.

이 연구는 공학(Engineering science)과 실험심리학(Experimental psychology) 분야 전문성을 결합해 진행한다. 연구팀은 참가자가 폴스타 차량을 운전하는 동안 나타나는 생리적, 인지적, 행동적 반응을 분석할 예정이다. 특히 뇌 활동, 생체 데이터, 운전 행동 데이터를 정밀 분석해 운전자가 느끼는 흥분감과 즐거움이 실제로 관찰, 분석 및 정량화될 수 있는지 확인할 계획이다.

알렉산더 베츠 옥스퍼드대 부총장 겸 SDG 임팩트 랩 공동 설립자는 "이번 프로젝트는 학술 연구가 실제 산업과 사회에 어떻게 영향을 미칠 수 있는지를 보여주는 좋은 사례"라며 "폴스타와 협업을 통해 과학적 분석과 통찰을 미래의 혁신을 이끌어갈 지식으로 전환할 수 있을 것"이라고 말했다.

", + "tags": [ + "네이버뉴스", + "생활/문화" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:33:26.000Z" + }, + "createdAt": "2026-05-26T00:12:57.444Z", + "updatedAt": "2026-05-26T00:12:57.444Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-b3173c5abf8645fc", + "title": "증권사·금감원 ‘위조사원증’까지 보여주며…현금·골드바 가로챈 ‘수거책’ 송치", + "sourceLabel": "한겨레", + "url": "https://n.news.naver.com/mnews/article/028/0002806129", + "lead": "투자리딩 피싱 수거에 이용된 위조 신분증. 서울경찰청 제공 주식투자 리딩방 사기나 기관 사칭 보이스피싱 조직의 지시를 받아 증권사 직원, 검찰청 직원 등을 사칭해 피해자들에게 현금과 골드바 등을 건네받는 역할을 한 소위 ‘수거책’들이 무더기로 검찰에 넘겨졌다. 이들은 피해자를 직접 찾아가 위조된 사원증까지 보여주는 방식으로 피해자들을 꾀어낸 것으로 조사됐다.", + "body": "투자리딩 피싱 수거에 이용된 위조 신분증. 서울경찰청 제공 주식투자 리딩방 사기나 기관 사칭 보이스피싱 조직의 지시를 받아 증권사 직원, 검찰청 직원 등을 사칭해 피해자들에게 현금과 골드바 등을 건네받는 역할을 한 소위 ‘수거책’들이 무더기로 검찰에 넘겨졌다. 이들은 피해자를 직접 찾아가 위조된 사원증까지 보여주는 방식으로 피해자들을 꾀어낸 것으로 조사됐다.\n\n서울경찰청 금융범죄수사대는 전기통신금융사기피해 환급법 위반 혐의를 받는 ㄱ(30대)씨 등 10명을 구속 송치했다고 21일 밝혔다. 이들은 지난 1월부터 3월까지 국외에 거점을 둔 주식투자 리딩방 사기 조직이나 보이스피싱 조직의 지시를 받아 피해자들로부터 각각 3억8520만원의 현금, 11억8천만원의 골드바를 가로챈 혐의를 받는다.\n\n경찰 설명을 들어보면, 리딩방 사기 조직의 경우 유명 주식 전문가의 유튜브 방송을 도용해 투자자를 모집한 뒤 가짜 금융 상품을 소개하고 ‘비밀 프로젝트여서 계좌로 돈을 이체하면 금융 감독원의 모니터링에 걸린다’며 피해자들을 속였다. 이후 위조된 증권사 직원 신분증 등을 가진 조직원(수거책)들이 피해자를 직접 찾아 투자금을 받아냈다. 동시에 허위로 개설한 증권사 투자앱에 투자금이 입금된 화면을 직접 보여주는 식으로 피해자들을 안심시켰다고 한다.\n\n보이스피싱 조직도 비슷한 방식으로 운영됐다. 이들은 피해자들에게 ‘범죄에 연루돼 구속영장이 발부됐다’고 겁을 준 뒤 남은 자산을 보호해주겠다며 골드바를 구매하도록 했다. 이후 수거책들은 위조된 검찰청 직원 신분증을 직접 피해자에게 보여주며 골드바를 건네 받았다.\n\n이들 조직에서 수거책으로 활동한 이들은 텔레그램 등에서 ‘고액알바’ 홍보 글을 보고 범행에 가담한 이들로, 숙련도에 따라 일당 30만원∼200만원을 받은 것으로 조사됐다. 피해자들은 피싱 범죄에 대한 최신 정보가 부족한 50∼60대 이상의 고령자가 대부분이었다고 한다. 경찰은 이들 수거책에 대한 수사를 시작으로, 범행을 지시한 윗선 조직을 추적 중이다.\n\n경찰 관계자는 “텔레그램 등에 단순 ‘고액알바’ 등으로 소개되는 경우 실제로는 사기 피해금을 수거하는 범죄 행위가 대부분이므로 절대 응해서는 안 된다”며 “또 어떠한 경우에도 금융기관이나 검찰청 등 국가기관에서 현금·골드바 등을 직접 받아가는 경우는 없으니 이를 요구한다면 각별한 주의가 필요하다”고 당부했다.", + "htmlBody": "
\"투자리딩

투자리딩 피싱 수거에 이용된 위조 신분증. 서울경찰청 제공 주식투자 리딩방 사기나 기관 사칭 보이스피싱 조직의 지시를 받아 증권사 직원, 검찰청 직원 등을 사칭해 피해자들에게 현금과 골드바 등을 건네받는 역할을 한 소위 ‘수거책’들이 무더기로 검찰에 넘겨졌다. 이들은 피해자를 직접 찾아가 위조된 사원증까지 보여주는 방식으로 피해자들을 꾀어낸 것으로 조사됐다.

서울경찰청 금융범죄수사대는 전기통신금융사기피해 환급법 위반 혐의를 받는 ㄱ(30대)씨 등 10명을 구속 송치했다고 21일 밝혔다. 이들은 지난 1월부터 3월까지 국외에 거점을 둔 주식투자 리딩방 사기 조직이나 보이스피싱 조직의 지시를 받아 피해자들로부터 각각 3억8520만원의 현금, 11억8천만원의 골드바를 가로챈 혐의를 받는다.

경찰 설명을 들어보면, 리딩방 사기 조직의 경우 유명 주식 전문가의 유튜브 방송을 도용해 투자자를 모집한 뒤 가짜 금융 상품을 소개하고 ‘비밀 프로젝트여서 계좌로 돈을 이체하면 금융 감독원의 모니터링에 걸린다’며 피해자들을 속였다. 이후 위조된 증권사 직원 신분증 등을 가진 조직원(수거책)들이 피해자를 직접 찾아 투자금을 받아냈다. 동시에 허위로 개설한 증권사 투자앱에 투자금이 입금된 화면을 직접 보여주는 식으로 피해자들을 안심시켰다고 한다.

보이스피싱 조직도 비슷한 방식으로 운영됐다. 이들은 피해자들에게 ‘범죄에 연루돼 구속영장이 발부됐다’고 겁을 준 뒤 남은 자산을 보호해주겠다며 골드바를 구매하도록 했다. 이후 수거책들은 위조된 검찰청 직원 신분증을 직접 피해자에게 보여주며 골드바를 건네 받았다.

이들 조직에서 수거책으로 활동한 이들은 텔레그램 등에서 ‘고액알바’ 홍보 글을 보고 범행에 가담한 이들로, 숙련도에 따라 일당 30만원∼200만원을 받은 것으로 조사됐다. 피해자들은 피싱 범죄에 대한 최신 정보가 부족한 50∼60대 이상의 고령자가 대부분이었다고 한다. 경찰은 이들 수거책에 대한 수사를 시작으로, 범행을 지시한 윗선 조직을 추적 중이다.

경찰 관계자는 “텔레그램 등에 단순 ‘고액알바’ 등으로 소개되는 경우 실제로는 사기 피해금을 수거하는 범죄 행위가 대부분이므로 절대 응해서는 안 된다”며 “또 어떠한 경우에도 금융기관이나 검찰청 등 국가기관에서 현금·골드바 등을 직접 받아가는 경우는 없으니 이를 요구한다면 각별한 주의가 필요하다”고 당부했다.

", + "tags": [ + "네이버뉴스", + "사회" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T06:22:22.000Z" + }, + "createdAt": "2026-05-26T00:12:56.977Z", + "updatedAt": "2026-05-26T00:12:56.977Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-ad1a47df7180b486", + "title": "서울 아파트값 0.31% 상승...3주 연속 상승폭 커져", + "sourceLabel": "YTN", + "url": "https://n.news.naver.com/mnews/article/052/0002356186", + "lead": "급매물이 사라지고 중저가 지역 키 맞추기 현상이 계속되는 가운데 서울 아파트값이 3주 연속 오름폭을 키웠습니다.", + "body": "급매물이 사라지고 중저가 지역 키 맞추기 현상이 계속되는 가운데 서울 아파트값이 3주 연속 오름폭을 키웠습니다.\n\n한국부동산원이 오늘(21일) 발표한 5월 셋째 주 주간 아파트 가격 동향을 보면 서울 아파트 매매가격은 전주 대비 평균 0.31% 상승했습니다.\n\n지난주보다 0.03%p 커진 것으로, 서울 주간 아파트값은 다주택자 양도세 중과 부활 전후로 3주 연속 상승폭을 키우는 모습입니다.\n\n자치구 가운데 서울 성북구가 전주보다 0.49% 오르며 가장 많이 올랐고 서대문구 0.46%, 강북구와 관악구가 0.45%, 강서구도 0.43% 껑충 뛰었습니다.\n\n전문가들은 전반적으로 매물이 감소한 가운데 대출 용이성과 저가 매물, 양호한 가격 접근성으로 해당 지역에 무주택 1인 가구와 신혼부부 등 실수요 유입이 꾸준히 발생하고 있다고 설명했습니다.\n\n서초구는 0.17%에서 0.26% 강남 0.19%에서 0.20%, 송파는 0.35%에서 0.38% 오르며 강남권도 모두 상승폭이 커졌습니다.\n\n서울 전세 상승률은 0.29%로 전주 대비 0.01%포인트 또 확대돼 지난 2015년 11월 둘째 주 이후 약 10년 6개월 만에 가장 높은 수준을 보였습니다.\n\n※ '당신의 제보가 뉴스가 됩니다' [카카오톡] YTN 검색해 채널 추가 [전화] 02-398-8585 [메일] social@ytn.co.kr", + "htmlBody": "

급매물이 사라지고 중저가 지역 키 맞추기 현상이 계속되는 가운데 서울 아파트값이 3주 연속 오름폭을 키웠습니다.

한국부동산원이 오늘(21일) 발표한 5월 셋째 주 주간 아파트 가격 동향을 보면 서울 아파트 매매가격은 전주 대비 평균 0.31% 상승했습니다.

지난주보다 0.03%p 커진 것으로, 서울 주간 아파트값은 다주택자 양도세 중과 부활 전후로 3주 연속 상승폭을 키우는 모습입니다.

자치구 가운데 서울 성북구가 전주보다 0.49% 오르며 가장 많이 올랐고 서대문구 0.46%, 강북구와 관악구가 0.45%, 강서구도 0.43% 껑충 뛰었습니다.

전문가들은 전반적으로 매물이 감소한 가운데 대출 용이성과 저가 매물, 양호한 가격 접근성으로 해당 지역에 무주택 1인 가구와 신혼부부 등 실수요 유입이 꾸준히 발생하고 있다고 설명했습니다.

서초구는 0.17%에서 0.26% 강남 0.19%에서 0.20%, 송파는 0.35%에서 0.38% 오르며 강남권도 모두 상승폭이 커졌습니다.

서울 전세 상승률은 0.29%로 전주 대비 0.01%포인트 또 확대돼 지난 2015년 11월 둘째 주 이후 약 10년 6개월 만에 가장 높은 수준을 보였습니다.

※ '당신의 제보가 뉴스가 됩니다' [카카오톡] YTN 검색해 채널 추가 [전화] 02-398-8585 [메일] social@ytn.co.kr

", + "tags": [ + "네이버뉴스", + "경제" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T06:21:18.000Z" + }, + "createdAt": "2026-05-26T00:12:56.288Z", + "updatedAt": "2026-05-26T00:12:56.288Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-8b2efecbc801a252", + "title": "\"북구의 삶이 소명이고 책무\" 출사표 낸 하정우 [TF사진관]", + "sourceLabel": "더팩트", + "url": "https://n.news.naver.com/mnews/article/629/0000501070", + "lead": "하정우 더불어민주당 부산 북구갑 국회의원 후보가 21일 오전 부산 북구 구포대교 사거리에서 출정식을 열고 발언하고 있다. /부산=박상민 기자", + "body": "하정우 더불어민주당 부산 북구갑 국회의원 후보가 21일 오전 부산 북구 구포대교 사거리에서 출정식을 열고 발언하고 있다. /부산=박상민 기자\n\n[더팩트 | 부산=박상민 기자] 하정우 더불어민주당 부산 북구갑 국회의원 후보가 21일 오전 부산 북구 구포대교 사거리에서 출정식을 열고 발언하고 있다.\n\n이날 하 후보는 출정식에서 \"많은 분들이 이구동성으로 저를 '돌아온 북구의 아들'이라 불러주셨다\"며 \"제 이름 앞에 그 하나면 충분하다\"고 말했다. 이어 \"오직 북구 발전이라는 목표를 향해 전력 질주 하겠다\"며 \"학교와 기업, 청와대에서 쌓은 인맥을 이제 북구를 바꾸는 확실한 변화로 만들겠다\"고 선언했다.\n\n하 후보는 한동훈 무소속 후보를 겨냥한 듯 \"'보수 복구' 이런 것은 서울 가서 하시길 바란다\"고 비판하며 \"북구라는 이름에 무슨 정치고, 이념이고, 정파가 있냐\"고 강조했다.\n\n하 후보는 이날 출정식에 이어 남산정사회복지관을 방문해 콩국수 나눔 행사에 참여할 계획이다.\n\n발로 뛰는 더팩트는 24시간 여러분의 제보를 기다립니다. ▶카카오톡: '더팩트제보' 검색 ▶이메일: jebo@tf.co.kr ▶뉴스 홈페이지: http://talk.tf.co.kr/bbs/report/write", + "htmlBody": "
\"하정우

하정우 더불어민주당 부산 북구갑 국회의원 후보가 21일 오전 부산 북구 구포대교 사거리에서 출정식을 열고 발언하고 있다. /부산=박상민 기자

\"기사

[더팩트 | 부산=박상민 기자] 하정우 더불어민주당 부산 북구갑 국회의원 후보가 21일 오전 부산 북구 구포대교 사거리에서 출정식을 열고 발언하고 있다.

\"기사
\"기사
\"기사

이날 하 후보는 출정식에서 "많은 분들이 이구동성으로 저를 '돌아온 북구의 아들'이라 불러주셨다"며 "제 이름 앞에 그 하나면 충분하다"고 말했다. 이어 "오직 북구 발전이라는 목표를 향해 전력 질주 하겠다"며 "학교와 기업, 청와대에서 쌓은 인맥을 이제 북구를 바꾸는 확실한 변화로 만들겠다"고 선언했다.

\"기사

하 후보는 한동훈 무소속 후보를 겨냥한 듯 "'보수 복구' 이런 것은 서울 가서 하시길 바란다"고 비판하며 "북구라는 이름에 무슨 정치고, 이념이고, 정파가 있냐"고 강조했다.

하 후보는 이날 출정식에 이어 남산정사회복지관을 방문해 콩국수 나눔 행사에 참여할 계획이다.

\"기사
\"기사
\"기사
\"기사
\"기사

발로 뛰는 더팩트는 24시간 여러분의 제보를 기다립니다. ▶카카오톡: '더팩트제보' 검색 ▶이메일: jebo@tf.co.kr ▶뉴스 홈페이지: http://talk.tf.co.kr/bbs/report/write

", + "tags": [ + "네이버뉴스", + "정치" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-20T23:32:11.000Z" + }, + "createdAt": "2026-05-26T00:11:28.738Z", + "updatedAt": "2026-05-26T00:11:28.738Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-3efb97ab2c8ced3a", + "title": "\"평창\" 2038 동계올림픽 독자 유치 추진…심재국 후보 공약 현실성은?", + "sourceLabel": "데일리안", + "url": "https://n.news.naver.com/mnews/article/119/0003092818", + "lead": "“평창 미래 100년 성장전략” 독자 개최 의지 평창 대회 시설 적자 심각한 수준, 유지비 등 사후 관리도 문제 기존 인프라 활용한 재유치 사례 나와, KTX 등 도로망은 이미 구축", + "body": "“평창 미래 100년 성장전략” 독자 개최 의지 평창 대회 시설 적자 심각한 수준, 유지비 등 사후 관리도 문제 기존 인프라 활용한 재유치 사례 나와, KTX 등 도로망은 이미 구축\n\n최근 5년 간 평창 동계올림픽경기장 손익현황. ⓒ 강원특별자치도 [데일리안 = 김평호 기자] 동계올림픽 독자 유치 공약은 과연 얼마나 현실성이 있을까.\n\n국민의힘 심재국 평창군수 후보가 2038 평창 동계올림픽을 유치해 독자 개최 시대를 열겠다고 공약해 눈길을 모으고 있다.\n\n심재국 후보는 지난 15일 평창올림픽플라자에서 열린 ‘평창 올림픽 유산의 완성과 2038 동계올림픽 독자 개최 전략’ 포럼서 비전 발표를 통해 “우리는 2018 평창동계올림픽을 성공시켰고 세계를 감동시켰다. 2038 동계올림픽은 단순한 재도전이 아니라 대한민국 동계스포츠의 미래 전략이자 평창 미래 100년 성장전략”이라며 독자 개최 의지를 강조했다.\n\n앞서 2018 평창 동계올림픽과 패럴림픽, 청소년동계올림픽까지 성공적으로 치러낸 경험을 강조한 심 후보는 “올림픽 개최도시 총회를 통해 평창은 이미 세계가 인정한 대한민국 대표 동계올림픽 도시”라며 평창이 주도하는 진정한 ‘독자 올림픽’ 유치 의지를 전하기도 했다.\n\n8년 전 국내서 열린 평창 동계올림픽은 ‘평창’이라는 지명이 들어가긴 했지만 설상경기장을 갖춘 평창군과 빙상경기장이 위치한 강릉시, 알파인경기장의 정선군에서 분산 개최돼 평창군 입장에서는 온전한 특수 효과를 누리지 못해 다소 아쉬움이 클 수밖에 없었다.\n\n특히 평창군과 강릉시 두 지역은 올림픽 명칭과 개폐회식 장소를 놓고 갈등을 빚기도 했다.\n\n심 후보의 바람대로 독자 개최가 이뤄진다면 ‘평창군’이라는 브랜드를 세계에 알리며 가치를 끌어올릴 수 있고 이에 따른 경제적 파급 효과도 기대할 수 있다.\n\n국민의힘 심재국 평창군수 후보. ⓒ 데일리안DB 문제는 결국 돈이다.\n\n이미 지어진 올림픽 유산을 재활용할 수 있다는 이점이 있지만 해당 시설들의 적자는 심각한 수준으로 전해진다.\n\n실제 강원도에 따르면 평창동계올림픽 유산인 경기장 6곳은 최근 5년 간 손실액이 2020년 54억2200만원, 2021년 62억4400만원, 2022년 62억6400만원, 2023년 67억4400만원, 2024년 57억7500만원으로 해마다 증가하며 심각한 운영 적자를 기록 중이다.\n\n2038 동계올림픽 독자 유치를 위해 상대적으로 부족한 빙상경기장을 신설하려면 예산이 더 필요하고, 유지비 등 사후 관리도 문제다.\n\n‘평창 올림픽 유산의 완성과 2038 동계올림픽 독자 개최 전략 포럼’이 지난 15일 평창군 평창올림픽플라자에서 유상범 국회의원, 심재국 국민의 힘 평창군수 후보, 스키협회 관계자, 선수, 지도자 등이 참석한 가운데 열렸다. ⓒ심재국 후보 선거사무소 다만 최근에는 기존 시설을 100% 활용이 가능하다는 장점을 앞세워 올림픽을 재유치하는 사례도 나온다.\n\n2002년 동계올림픽을 유치했던 솔트레이크시티는 2034년 동계올림픽 개최지로 선정돼 32년 만에 다시 축제를 연다.\n\n평창서 동계올림픽을 다시 여는 것도 무리는 아니라는 평가다.\n\n심재국 후보 캠프 관계자는 “동계올림픽 같은 경우 투자비가 거의 교통 등 도로망 구축 비중이 크다. 그런데 이 부분은 이미 KTX 등 구성이 돼 있는 상태”라면서 “적자 올림픽에 대한 우려가 있는데 투자 대비 염려할 부분은 아니다”라고 강조했다.\n\n이어 “부족한 것은 실내 빙상경기장이다. 이것을 관내로 가지고 들어와서 독자 올림픽을 추진하겠다는 구상이다. 동계올림픽 인프라를 가지고 어떻게 우리 스포츠 미래를 운영할 것인가를 계획하고 있다. 평창을 스포츠 꿈나무들 키우는 메카로 만들어 적자에 대한 부분들도 상쇄할 수 있다”고 전했다.\n\n끝으로 “흑자 올림픽으로 가는 방향이 될 것이다. 앞서 언급한대로 가장 큰 비용이 들어가는 교통망 시설은 기존에 어느 정도 돼 있다. 기존 인프라를 가지고 재추진하는 부분들은 IOC에서도 선호하는 입장”이라며 “염려되는 부분은 알겠는데 크게 좌지우지되지는 않는다. 2018년처럼 큰 규모의 사업비가 투자되는 것이 아니다. 평창 때 KTX 구축에만 3조 원 정도 들었는데 지금은 전체 다 합해도 그 정도까지는 들어가지 않는다”고 말했다.", + "htmlBody": "

“평창 미래 100년 성장전략” 독자 개최 의지 평창 대회 시설 적자 심각한 수준, 유지비 등 사후 관리도 문제 기존 인프라 활용한 재유치 사례 나와, KTX 등 도로망은 이미 구축

\"최근

최근 5년 간 평창 동계올림픽경기장 손익현황. ⓒ 강원특별자치도 [데일리안 = 김평호 기자] 동계올림픽 독자 유치 공약은 과연 얼마나 현실성이 있을까.

국민의힘 심재국 평창군수 후보가 2038 평창 동계올림픽을 유치해 독자 개최 시대를 열겠다고 공약해 눈길을 모으고 있다.

심재국 후보는 지난 15일 평창올림픽플라자에서 열린 ‘평창 올림픽 유산의 완성과 2038 동계올림픽 독자 개최 전략’ 포럼서 비전 발표를 통해 “우리는 2018 평창동계올림픽을 성공시켰고 세계를 감동시켰다. 2038 동계올림픽은 단순한 재도전이 아니라 대한민국 동계스포츠의 미래 전략이자 평창 미래 100년 성장전략”이라며 독자 개최 의지를 강조했다.

앞서 2018 평창 동계올림픽과 패럴림픽, 청소년동계올림픽까지 성공적으로 치러낸 경험을 강조한 심 후보는 “올림픽 개최도시 총회를 통해 평창은 이미 세계가 인정한 대한민국 대표 동계올림픽 도시”라며 평창이 주도하는 진정한 ‘독자 올림픽’ 유치 의지를 전하기도 했다.

8년 전 국내서 열린 평창 동계올림픽은 ‘평창’이라는 지명이 들어가긴 했지만 설상경기장을 갖춘 평창군과 빙상경기장이 위치한 강릉시, 알파인경기장의 정선군에서 분산 개최돼 평창군 입장에서는 온전한 특수 효과를 누리지 못해 다소 아쉬움이 클 수밖에 없었다.

특히 평창군과 강릉시 두 지역은 올림픽 명칭과 개폐회식 장소를 놓고 갈등을 빚기도 했다.

심 후보의 바람대로 독자 개최가 이뤄진다면 ‘평창군’이라는 브랜드를 세계에 알리며 가치를 끌어올릴 수 있고 이에 따른 경제적 파급 효과도 기대할 수 있다.

\"국민의힘

국민의힘 심재국 평창군수 후보. ⓒ 데일리안DB 문제는 결국 돈이다.

이미 지어진 올림픽 유산을 재활용할 수 있다는 이점이 있지만 해당 시설들의 적자는 심각한 수준으로 전해진다.

실제 강원도에 따르면 평창동계올림픽 유산인 경기장 6곳은 최근 5년 간 손실액이 2020년 54억2200만원, 2021년 62억4400만원, 2022년 62억6400만원, 2023년 67억4400만원, 2024년 57억7500만원으로 해마다 증가하며 심각한 운영 적자를 기록 중이다.

2038 동계올림픽 독자 유치를 위해 상대적으로 부족한 빙상경기장을 신설하려면 예산이 더 필요하고, 유지비 등 사후 관리도 문제다.

\"‘평창

‘평창 올림픽 유산의 완성과 2038 동계올림픽 독자 개최 전략 포럼’이 지난 15일 평창군 평창올림픽플라자에서 유상범 국회의원, 심재국 국민의 힘 평창군수 후보, 스키협회 관계자, 선수, 지도자 등이 참석한 가운데 열렸다. ⓒ심재국 후보 선거사무소 다만 최근에는 기존 시설을 100% 활용이 가능하다는 장점을 앞세워 올림픽을 재유치하는 사례도 나온다.

2002년 동계올림픽을 유치했던 솔트레이크시티는 2034년 동계올림픽 개최지로 선정돼 32년 만에 다시 축제를 연다.

평창서 동계올림픽을 다시 여는 것도 무리는 아니라는 평가다.

심재국 후보 캠프 관계자는 “동계올림픽 같은 경우 투자비가 거의 교통 등 도로망 구축 비중이 크다. 그런데 이 부분은 이미 KTX 등 구성이 돼 있는 상태”라면서 “적자 올림픽에 대한 우려가 있는데 투자 대비 염려할 부분은 아니다”라고 강조했다.

이어 “부족한 것은 실내 빙상경기장이다. 이것을 관내로 가지고 들어와서 독자 올림픽을 추진하겠다는 구상이다. 동계올림픽 인프라를 가지고 어떻게 우리 스포츠 미래를 운영할 것인가를 계획하고 있다. 평창을 스포츠 꿈나무들 키우는 메카로 만들어 적자에 대한 부분들도 상쇄할 수 있다”고 전했다.

끝으로 “흑자 올림픽으로 가는 방향이 될 것이다. 앞서 언급한대로 가장 큰 비용이 들어가는 교통망 시설은 기존에 어느 정도 돼 있다. 기존 인프라를 가지고 재추진하는 부분들은 IOC에서도 선호하는 입장”이라며 “염려되는 부분은 알겠는데 크게 좌지우지되지는 않는다. 2018년처럼 큰 규모의 사업비가 투자되는 것이 아니다. 평창 때 KTX 구축에만 3조 원 정도 들었는데 지금은 전체 다 합해도 그 정도까지는 들어가지 않는다”고 말했다.

", + "tags": [ + "네이버뉴스", + "사회" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T00:19:07.000Z" + }, + "createdAt": "2026-05-26T00:11:28.454Z", + "updatedAt": "2026-05-26T00:11:28.454Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-e4062de9188f9c09", + "title": "카스트로 기소하고 항모 보내고는…트럼프 \"쿠바와 긴장 고조 없다\"", + "sourceLabel": "뉴스1", + "url": "https://n.news.naver.com/mnews/article/421/0008957803", + "lead": "쿠바 혁명 상징 라울 카스트로, 30년 전 민항기 격추 혐의로 기소 베네수엘라 마두로 체포 '데자뷔'…카리브해엔 美 항모 전단 배치", + "body": "쿠바 혁명 상징 라울 카스트로, 30년 전 민항기 격추 혐의로 기소 베네수엘라 마두로 체포 '데자뷔'…카리브해엔 美 항모 전단 배치\n\n도널드 트럼프 미국 대통령. 2026.05.16. ⓒ 로이터=뉴스1\n\n(서울=뉴스1) 강민경 기자 = 미국의 쿠바의 '막후 실세' 라울 카스트로(94) 전 국가평의회 의장을 기소한 20일(현지시간) 도널드 트럼프 대통령이 쿠바와의 군사적 긴장 고조 가능성을 일축했다.\n\n로이터통신 등에 따르면 트럼프 대통령은 20일(현지시간) 백악관에서 기자들과 만나 \"긴장 고조는 없을 것이다. 그럴 필요도 없다\"며 \"쿠바는 무너져가고 있고 통제력을 잃었다\"고 말했다. 그는 \"미국의 목표는 쿠바를 해방시키는 것\"이라고도 주장했다.\n\n같은 날 미 법무부는 카스트로를 전격 기소했다고 발표했다.\n\n30년 전인 1996년 2월 당시 쿠바 국방장관이던 카스트로가 미 마이애미에 기반을 둔 쿠바 망명자 단체 '구조 형제단' 소속 민간 경비행기 2대를 격추하라고 지시해 미국 시민 3명을 포함한 4명을 숨지게 한 혐의(살인 및 살인 공모)가 적용됐다.\n\n역사적으로 미국이 외국의 전직 국가 정상을 기소한 것은 상당히 이례적인 일로 평가된다.\n\n트럼프 대통령의 말과 달리 쿠바에 대한 미국의 군사적 압박 수위는 최고조에 이르고 있다.\n\n라울 카스트로 전 쿠바 국가평의회 의장이 2025년 5월 하바나에서 열린 메이데이 집회를 지켜보고 있다. 2025.5.1 ⓒ 로이터=뉴스1\n\n카스트로 기소 발표와 동시에 미 남부사령부는 항공모함 USS 니미츠를 포함한 항모 타격단이 카리브해에 도착했다고 밝혔다. 쿠바를 사정권에 둔 명백한 무력시위로 해석된다.\n\n이번 기소는 지난 1월 미국의 군사 작전으로 체포된 니콜라스 마두로 전 베네수엘라 대통령의 사례를 떠올리게 한다는 평가가 나온다. 당시에도 미국은 마두로를 마약 밀매 혐의 등으로 먼저 기소한 뒤, 이를 명분으로 군사 작전을 감행해 신병을 확보했다.\n\n이 때문에 미국이 쿠바에도 동일한 정권 교체 시나리오를 적용하려는 것 아니냐는 관측도 나온다.\n\n실제로 트럼프 행정부는 마두로 체포 이후 쿠바에 대한 압박을 꾸준히 강화해 왔다. 쿠바에 석유를 공급하는 모든 국가에 관세를 부과한다는 행정명령에 서명하며 돈줄까지 끊어 놨다.\n\n지난 2월 트럼프 대통령은 쿠바를 \"우호적으로 인수할 수 있다\"고까지 발언하며 사실상 체제 전복 가능성을 시사했다.\n\n미국의 고강도 압박에 쿠바는 강하게 반발하고 있다. 미겔 디아스카넬 쿠바 대통령은 이번 기소에 대해 \"어떠한 법적 근거도 없는 정치적 모략\"이라며 미국의 군사공격이 이뤄진다면 \"라틴아메리카의 평화와 안정을 파괴하는 피바다를 초래할 것\"이라고 경고했다.", + "htmlBody": "

쿠바 혁명 상징 라울 카스트로, 30년 전 민항기 격추 혐의로 기소 베네수엘라 마두로 체포 '데자뷔'…카리브해엔 美 항모 전단 배치

\"도널드

도널드 트럼프 미국 대통령. 2026.05.16. ⓒ 로이터=뉴스1

(서울=뉴스1) 강민경 기자 = 미국의 쿠바의 '막후 실세' 라울 카스트로(94) 전 국가평의회 의장을 기소한 20일(현지시간) 도널드 트럼프 대통령이 쿠바와의 군사적 긴장 고조 가능성을 일축했다.

로이터통신 등에 따르면 트럼프 대통령은 20일(현지시간) 백악관에서 기자들과 만나 "긴장 고조는 없을 것이다. 그럴 필요도 없다"며 "쿠바는 무너져가고 있고 통제력을 잃었다"고 말했다. 그는 "미국의 목표는 쿠바를 해방시키는 것"이라고도 주장했다.

같은 날 미 법무부는 카스트로를 전격 기소했다고 발표했다.

30년 전인 1996년 2월 당시 쿠바 국방장관이던 카스트로가 미 마이애미에 기반을 둔 쿠바 망명자 단체 '구조 형제단' 소속 민간 경비행기 2대를 격추하라고 지시해 미국 시민 3명을 포함한 4명을 숨지게 한 혐의(살인 및 살인 공모)가 적용됐다.

역사적으로 미국이 외국의 전직 국가 정상을 기소한 것은 상당히 이례적인 일로 평가된다.

트럼프 대통령의 말과 달리 쿠바에 대한 미국의 군사적 압박 수위는 최고조에 이르고 있다.

\"라울

라울 카스트로 전 쿠바 국가평의회 의장이 2025년 5월 하바나에서 열린 메이데이 집회를 지켜보고 있다. 2025.5.1 ⓒ 로이터=뉴스1

카스트로 기소 발표와 동시에 미 남부사령부는 항공모함 USS 니미츠를 포함한 항모 타격단이 카리브해에 도착했다고 밝혔다. 쿠바를 사정권에 둔 명백한 무력시위로 해석된다.

이번 기소는 지난 1월 미국의 군사 작전으로 체포된 니콜라스 마두로 전 베네수엘라 대통령의 사례를 떠올리게 한다는 평가가 나온다. 당시에도 미국은 마두로를 마약 밀매 혐의 등으로 먼저 기소한 뒤, 이를 명분으로 군사 작전을 감행해 신병을 확보했다.

이 때문에 미국이 쿠바에도 동일한 정권 교체 시나리오를 적용하려는 것 아니냐는 관측도 나온다.

실제로 트럼프 행정부는 마두로 체포 이후 쿠바에 대한 압박을 꾸준히 강화해 왔다. 쿠바에 석유를 공급하는 모든 국가에 관세를 부과한다는 행정명령에 서명하며 돈줄까지 끊어 놨다.

지난 2월 트럼프 대통령은 쿠바를 "우호적으로 인수할 수 있다"고까지 발언하며 사실상 체제 전복 가능성을 시사했다.

미국의 고강도 압박에 쿠바는 강하게 반발하고 있다. 미겔 디아스카넬 쿠바 대통령은 이번 기소에 대해 "어떠한 법적 근거도 없는 정치적 모략"이라며 미국의 군사공격이 이뤄진다면 "라틴아메리카의 평화와 안정을 파괴하는 피바다를 초래할 것"이라고 경고했다.

", + "tags": [ + "네이버뉴스", + "세계" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T00:18:29.000Z" + }, + "createdAt": "2026-05-26T00:11:27.872Z", + "updatedAt": "2026-05-26T00:11:27.872Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-ff241d5ef8520158", + "title": "조응천 “양향자 찍으면 장동혁 체제 연장... 張 무너져야 與 폭주 막아”", + "sourceLabel": "조선일보", + "url": "https://n.news.naver.com/mnews/article/023/0003977678", + "lead": "개혁신당 조응천 경기지사 후보는 21일 “국민의힘 양향자 경기지사 후보를 찍는 건 무능한 국민의힘 장동혁 대표 체제를 연장해주는 것”이라면서 더불어민주당·국민의힘 등 거대 양당이 아닌 자신에 대한 투표를 호소했다.", + "body": "개혁신당 조응천 경기지사 후보는 21일 “국민의힘 양향자 경기지사 후보를 찍는 건 무능한 국민의힘 장동혁 대표 체제를 연장해주는 것”이라면서 더불어민주당·국민의힘 등 거대 양당이 아닌 자신에 대한 투표를 호소했다.\n\n개혁신당 조응천 경기지사 후보가 21일 경기 화성시 동탄에서 열린 중앙선대위원회의 및 경기도 선대위원회 출정식에서 말하고 있다. /연합뉴스 조응천 후보는 이날 경기 화성 동탄에 있는 개혁신당 전성균 화성시장 후보 캠프 사무실에서 열린 중앙선대위·경기도선대위 출정식에서 “양향자 후보를 찍어 장동혁 체제를 연장해 주는 것은 파렴치한 이재명·정청래·추미애가 이끄는 민주당에 향후 20~30년 장기 집권의 고속도로를 열어주는 길”이라면서 이같이 말했다.\n\n조 후보는 “민주당 추미애 경기지사 후보가 싫다고 해서 ‘장동혁의 아바타’(양향자)에게 표를 던지는, 어리석은 사표를 만들지 말아 달라”며 “제3 정당의 입장에선 언제나 두 개의 거악과 맞서 싸워야 한다. 한쪽으로는 추미애라는 후보와 다른 한쪽으론 장동혁이라는 후보와 맞서 싸우고 있다”고 했다.\n\n그러면서 조 후보는 “제가 (경기지사 선거 득표율) 15%를 넘기면 장동혁 체제가 사정없이 휘청거릴 것이고, 20%를 돌파하는 순간 양당 구도는 순식간에 붕괴된다”며 “장 대표 체제가 무너져야 국민의힘이 바뀌고, 그래야 상식 있는 정치 세력이 자라나 민주당의 폭주를 막아낼 수 있다”고 했다.\n\n조 후보는 공식 선거운동 첫날인 이날 ‘경기남부 반도체 4대 인프라 패키지’를 공약했다. 이천·용인·화성·수원·평택 등 5대 거점과 평택항·경기남부국제공항을 잇는 도로·철도 병행 교통망인 ‘반도체 익스프레스’ 구축 등을 약속했다.", + "htmlBody": "

개혁신당 조응천 경기지사 후보는 21일 “국민의힘 양향자 경기지사 후보를 찍는 건 무능한 국민의힘 장동혁 대표 체제를 연장해주는 것”이라면서 더불어민주당·국민의힘 등 거대 양당이 아닌 자신에 대한 투표를 호소했다.

\"개혁신당

개혁신당 조응천 경기지사 후보가 21일 경기 화성시 동탄에서 열린 중앙선대위원회의 및 경기도 선대위원회 출정식에서 말하고 있다. /연합뉴스 조응천 후보는 이날 경기 화성 동탄에 있는 개혁신당 전성균 화성시장 후보 캠프 사무실에서 열린 중앙선대위·경기도선대위 출정식에서 “양향자 후보를 찍어 장동혁 체제를 연장해 주는 것은 파렴치한 이재명·정청래·추미애가 이끄는 민주당에 향후 20~30년 장기 집권의 고속도로를 열어주는 길”이라면서 이같이 말했다.

조 후보는 “민주당 추미애 경기지사 후보가 싫다고 해서 ‘장동혁의 아바타’(양향자)에게 표를 던지는, 어리석은 사표를 만들지 말아 달라”며 “제3 정당의 입장에선 언제나 두 개의 거악과 맞서 싸워야 한다. 한쪽으로는 추미애라는 후보와 다른 한쪽으론 장동혁이라는 후보와 맞서 싸우고 있다”고 했다.

그러면서 조 후보는 “제가 (경기지사 선거 득표율) 15%를 넘기면 장동혁 체제가 사정없이 휘청거릴 것이고, 20%를 돌파하는 순간 양당 구도는 순식간에 붕괴된다”며 “장 대표 체제가 무너져야 국민의힘이 바뀌고, 그래야 상식 있는 정치 세력이 자라나 민주당의 폭주를 막아낼 수 있다”고 했다.

조 후보는 공식 선거운동 첫날인 이날 ‘경기남부 반도체 4대 인프라 패키지’를 공약했다. 이천·용인·화성·수원·평택 등 5대 거점과 평택항·경기남부국제공항을 잇는 도로·철도 병행 교통망인 ‘반도체 익스프레스’ 구축 등을 약속했다.

", + "tags": [ + "네이버뉴스", + "정치" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:48:13.000Z" + }, + "createdAt": "2026-05-26T00:11:27.285Z", + "updatedAt": "2026-05-26T00:11:27.285Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-e5948698dbb3b5c8", + "title": "李대통령 \"GTX 철근누락 엄정한 실태 파악·철저한 안전점검\" 지시", + "sourceLabel": "뉴스1", + "url": "https://n.news.naver.com/mnews/article/421/0008958512", + "lead": "'서울 성동경찰서장 관용차 유용 의혹' 신속 감찰 지시", + "body": "'서울 성동경찰서장 관용차 유용 의혹' 신속 감찰 지시\n\n이재명 대통령이 21일 청와대에서 열린 대통령 자문회의·위원회 간담회에서 발언하고 있다. 2026.5.21 ⓒ 뉴스1 이재명 기자\n\n(서울=뉴스1) 이기림 김근욱 임윤지 기자 = 이재명 대통령은 21일 수도권광역급행철도(GTX)-A 삼성역 구간 공사 현장의 철근 누락 사태에 관한 관계 부처의 실태 파악을 지시했다.\n\n강유정 청와대 수석대변인은 이날 오전 청와대에서 열린 브리핑을 통해 \"(이 대통령은) 국토교통부, 행정안전부 등 관계부처의 엄정한 실태 파악과 철저한 안전 점검을 지시했다\"라고 밝혔다.\n\n국토부에 따르면 지난 15일 GTX-A 삼성역 구간에서 시공 오류 사항이 확인돼 정부는 긴급 현장점검과 후속 조치에 착수했다. 기둥 80본 가운데 일부에서 주철근 2열을 설치해야 하는 구조를 1열만 시공한 사실이 확인됐고, 이에 80본 중 50본이 준공 구조물 기준을 충족하지 못한 것으로 나타났다.\n\n강 수석대변인은 이 대통령의 해당 지시가 6·3 지방선거를 앞두고 논란이 될 수 있다는 지적에 대해 \"이 사태는 사실상 여름철 우기와 여러 가지 상황에서 앞으로의 상황 점검을 봤을 때, 대형 안전사고 방지 차원에서 현장의 안전을 살필 정부로서의 책임과 의무가 있고, 사태 발생 원인에 대해서 국토부 및 행안부 등 관련 부처에 대해서 검토할 것을 지시한 사안\"이라고 설명했다.\n\n또한 강 수석대변인은 \"(이 대통령은) 서울 성동경찰서장이 긴급출동 차량을 2부제 예외 관용 전기차로 유용한 사실을 취재한 SBS 보도 관련 보고를 받고, 신속 감찰을 통해 엄중 문책하고 공직 기강해이 사례가 발생하지 않도록 철저히 조치할 것을 지시했다\"라고 밝혔다.", + "htmlBody": "

'서울 성동경찰서장 관용차 유용 의혹' 신속 감찰 지시

\"이재명

이재명 대통령이 21일 청와대에서 열린 대통령 자문회의·위원회 간담회에서 발언하고 있다. 2026.5.21 ⓒ 뉴스1 이재명 기자

(서울=뉴스1) 이기림 김근욱 임윤지 기자 = 이재명 대통령은 21일 수도권광역급행철도(GTX)-A 삼성역 구간 공사 현장의 철근 누락 사태에 관한 관계 부처의 실태 파악을 지시했다.

강유정 청와대 수석대변인은 이날 오전 청와대에서 열린 브리핑을 통해 "(이 대통령은) 국토교통부, 행정안전부 등 관계부처의 엄정한 실태 파악과 철저한 안전 점검을 지시했다"라고 밝혔다.

국토부에 따르면 지난 15일 GTX-A 삼성역 구간에서 시공 오류 사항이 확인돼 정부는 긴급 현장점검과 후속 조치에 착수했다. 기둥 80본 가운데 일부에서 주철근 2열을 설치해야 하는 구조를 1열만 시공한 사실이 확인됐고, 이에 80본 중 50본이 준공 구조물 기준을 충족하지 못한 것으로 나타났다.

강 수석대변인은 이 대통령의 해당 지시가 6·3 지방선거를 앞두고 논란이 될 수 있다는 지적에 대해 "이 사태는 사실상 여름철 우기와 여러 가지 상황에서 앞으로의 상황 점검을 봤을 때, 대형 안전사고 방지 차원에서 현장의 안전을 살필 정부로서의 책임과 의무가 있고, 사태 발생 원인에 대해서 국토부 및 행안부 등 관련 부처에 대해서 검토할 것을 지시한 사안"이라고 설명했다.

또한 강 수석대변인은 "(이 대통령은) 서울 성동경찰서장이 긴급출동 차량을 2부제 예외 관용 전기차로 유용한 사실을 취재한 SBS 보도 관련 보고를 받고, 신속 감찰을 통해 엄중 문책하고 공직 기강해이 사례가 발생하지 않도록 철저히 조치할 것을 지시했다"라고 밝혔다.

", + "tags": [ + "네이버뉴스", + "정치" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:47:13.000Z" + }, + "createdAt": "2026-05-26T00:11:26.782Z", + "updatedAt": "2026-05-26T00:11:26.782Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-b95d7a545efc1482", + "title": "[신간] 조선후기 궁중건축화의 탄생…", + "sourceLabel": "연합뉴스", + "url": "https://n.news.naver.com/mnews/article/001/0016091213", + "lead": "'깨어 있는 척하는 기업들'·'영양제의 과학'", + "body": "'깨어 있는 척하는 기업들'·'영양제의 과학'\n\n(서울=연합뉴스) 김정은 기자 = ▲ 그림 속으로 들어온 궁궐과 도시 = 윤민용 지음.\n\n한국미술사 연구자인 저자가 조선 후기 궁중회화에서 보이는 갑작스럽고 거대한 시각적 혁신의 동인을 '동아시아 시각문화 교류'라는 관점에서 분석한 미술사 연구서다.\n\n조선의 궁중회화는 18세기 후반 정조대를 기점으로 변화하는데, 건축물과 도시를 주제로 한 회화와 시각 자료가 집중적으로 제작되기 시작한다.\n\n저자는 이러한 변화의 배경을 찾기 위해 조선을 넘어 17∼19세기 동아시아라는 거시적 관점에서 조선 후기 궁중 건축화의 탄생을 조망한다.\n\n실재하는 궁궐을 그린 궁궐도, 도시를 재현한 성시도, 상상의 궁궐을 그린 한궁도 등 건축물을 주제로 한 건축화 연구를 통해 그동안 보수적이라고 인식돼온 조선시대 궁중회화가 이웃 나라의 시각 자료를 어떻게 참조, 수용, 변형했는지 추적한다.\n\n청 궁정에서 유입된 서양 동판화, 일본에서 외교 선물로 유입된 회화병풍 등 동시대 동아시아 시각 자료 비교연구를 바탕으로 조선 궁중회화가 동아시아 시각문화의 흐름을 빠르게 포착해 능동적으로 발전해온 산물이었다고 강조한다.\n\n저자는 또 궁중회화에서 건축화가 단순히 건축물 이미지를 모방 또는 재현하는 것을 넘어 통치자의 이념과 위엄을 드러내는 고도의 '권력의 시각언어'로 작동했다고 말한다.\n\n2024년 제16회 심원건축학술상 수상작이다.\n\n스페이스타임(spacetime). 662쪽.\n\n▲ 깨어 있는 척하는 기업들 = 비벡 라마스와미 지음. 김태훈 옮김.\n\n미국으로 건너온 인도 이민자 가정에서 태어나 기업가에서 정치인으로 변신한 저자가 2001년 낸 책의 한국어판이다. 책의 원제는 '워크 주식회사'(Woke, Inc.)다.\n\n워크는 원래 인종·성 차별, 사회적 정의에 대한 각성을 의미했지만, 이후 미국 사회에서 과도한 정치적 올바름에 대한 반발이 일면서 보수 진영은 워크를 진보적 가치와 정체성을 강요하는 행위라는 비판적 의미로 사용하고 있다.\n\n저자는 미국 기업이 '워크 문화'를 이용해 자신들의 이익을 보호하고 부풀리고 있다고 비판하고, 자신이 정·재계 거물급 인사들을 만나며 겪었던 일화 등을 통해 그 실상을 이야기한다.\n\n저자는 이 같은 기업을 '워크 산업 복합체'라고 부르면서 이들은 도덕성과 상업주의를 뒤섞고, 사람들이 받아들여야 하는 사회적 가치에 대해 말하며 이를 돈벌이에 이용한다고 주장한다.\n\n특히 기업들이 시장 지배력을 이용해 도덕적 규칙을 만들고 정치적 선언을 하면서 그와 다른 목소리는 공개적으로 낼 수 없는 분위기가 만들어졌다고 지적한다.\n\n이는 결국 민주주의의 건전성을 해치고 당파 정치가 비정치적이었던 삶의 영역까지 파고들며 대중을 분열시킨다고 저자는 꼬집는다.\n\n▲ 영양제의 과학 = 크리스티네 기터 지음. 유영미 옮김.\n\n독일에서 30년 가까이 약국을 운영하는 저자가 영양제가 몸에서 어떤 작용을 하고, 어떻게 유익하게 활용할 수 있는지를 이야기한다.\n\n저자는 영양제는 자신에게 실제로 부족한 것이 무엇인지 파악하고 목표를 정해 섭취해야 효과를 볼 수 있다고 강조하면서 넘쳐나는 광고와 건강 정보에 휩쓸리지 않고 필요한 것만 챙길 수 있도록 하는 판단 기준을 제시한다.\n\n특히 영양제가 필요한 이유는 비타민, 칼슘, 철, 아연 등 미량영양소를 보충하기 위한 것이라고 지적하면서 중요한 미량영양소에 대해서도 살펴본다.\n\n저자는 영양제 섭취는 개인의 건강 상태를 고려해서 결정해야 한다고 강조한다. 미량영양소를 지혜롭게 보충하면 건강을 유지하고 컨디션을 개선할 수 있지만, 남들이 좋다고 하는 비타민과 무기질을 모두 챙겨 먹는다고 질병 예방에 도움이 되진 않는다고 말한다.", + "htmlBody": "

'깨어 있는 척하는 기업들'·'영양제의 과학'

\"기사

(서울=연합뉴스) 김정은 기자 = ▲ 그림 속으로 들어온 궁궐과 도시 = 윤민용 지음.

한국미술사 연구자인 저자가 조선 후기 궁중회화에서 보이는 갑작스럽고 거대한 시각적 혁신의 동인을 '동아시아 시각문화 교류'라는 관점에서 분석한 미술사 연구서다.

조선의 궁중회화는 18세기 후반 정조대를 기점으로 변화하는데, 건축물과 도시를 주제로 한 회화와 시각 자료가 집중적으로 제작되기 시작한다.

저자는 이러한 변화의 배경을 찾기 위해 조선을 넘어 17∼19세기 동아시아라는 거시적 관점에서 조선 후기 궁중 건축화의 탄생을 조망한다.

실재하는 궁궐을 그린 궁궐도, 도시를 재현한 성시도, 상상의 궁궐을 그린 한궁도 등 건축물을 주제로 한 건축화 연구를 통해 그동안 보수적이라고 인식돼온 조선시대 궁중회화가 이웃 나라의 시각 자료를 어떻게 참조, 수용, 변형했는지 추적한다.

청 궁정에서 유입된 서양 동판화, 일본에서 외교 선물로 유입된 회화병풍 등 동시대 동아시아 시각 자료 비교연구를 바탕으로 조선 궁중회화가 동아시아 시각문화의 흐름을 빠르게 포착해 능동적으로 발전해온 산물이었다고 강조한다.

저자는 또 궁중회화에서 건축화가 단순히 건축물 이미지를 모방 또는 재현하는 것을 넘어 통치자의 이념과 위엄을 드러내는 고도의 '권력의 시각언어'로 작동했다고 말한다.

2024년 제16회 심원건축학술상 수상작이다.

스페이스타임(spacetime). 662쪽.

\"기사

▲ 깨어 있는 척하는 기업들 = 비벡 라마스와미 지음. 김태훈 옮김.

미국으로 건너온 인도 이민자 가정에서 태어나 기업가에서 정치인으로 변신한 저자가 2001년 낸 책의 한국어판이다. 책의 원제는 '워크 주식회사'(Woke, Inc.)다.

워크는 원래 인종·성 차별, 사회적 정의에 대한 각성을 의미했지만, 이후 미국 사회에서 과도한 정치적 올바름에 대한 반발이 일면서 보수 진영은 워크를 진보적 가치와 정체성을 강요하는 행위라는 비판적 의미로 사용하고 있다.

저자는 미국 기업이 '워크 문화'를 이용해 자신들의 이익을 보호하고 부풀리고 있다고 비판하고, 자신이 정·재계 거물급 인사들을 만나며 겪었던 일화 등을 통해 그 실상을 이야기한다.

저자는 이 같은 기업을 '워크 산업 복합체'라고 부르면서 이들은 도덕성과 상업주의를 뒤섞고, 사람들이 받아들여야 하는 사회적 가치에 대해 말하며 이를 돈벌이에 이용한다고 주장한다.

특히 기업들이 시장 지배력을 이용해 도덕적 규칙을 만들고 정치적 선언을 하면서 그와 다른 목소리는 공개적으로 낼 수 없는 분위기가 만들어졌다고 지적한다.

이는 결국 민주주의의 건전성을 해치고 당파 정치가 비정치적이었던 삶의 영역까지 파고들며 대중을 분열시킨다고 저자는 꼬집는다.

\"기사

▲ 영양제의 과학 = 크리스티네 기터 지음. 유영미 옮김.

독일에서 30년 가까이 약국을 운영하는 저자가 영양제가 몸에서 어떤 작용을 하고, 어떻게 유익하게 활용할 수 있는지를 이야기한다.

저자는 영양제는 자신에게 실제로 부족한 것이 무엇인지 파악하고 목표를 정해 섭취해야 효과를 볼 수 있다고 강조하면서 넘쳐나는 광고와 건강 정보에 휩쓸리지 않고 필요한 것만 챙길 수 있도록 하는 판단 기준을 제시한다.

특히 영양제가 필요한 이유는 비타민, 칼슘, 철, 아연 등 미량영양소를 보충하기 위한 것이라고 지적하면서 중요한 미량영양소에 대해서도 살펴본다.

저자는 영양제 섭취는 개인의 건강 상태를 고려해서 결정해야 한다고 강조한다. 미량영양소를 지혜롭게 보충하면 건강을 유지하고 컨디션을 개선할 수 있지만, 남들이 좋다고 하는 비타민과 무기질을 모두 챙겨 먹는다고 질병 예방에 도움이 되진 않는다고 말한다.

", + "tags": [ + "네이버뉴스", + "생활/문화" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T02:38:45.000Z" + }, + "createdAt": "2026-05-26T00:11:26.222Z", + "updatedAt": "2026-05-26T00:11:26.222Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-f70f9d575f004cd1", + "title": "WTI 100달러 아래로…트럼프 \"이란협상 최종단계\", 이란 외무 \"검토중\"", + "sourceLabel": "뉴스1", + "url": "https://n.news.naver.com/mnews/article/421/0008957411", + "lead": "선박들이 지난 4월 22일 오만 무산담 인근 호르무즈 해협을 지나고 있다. 2026.05.19. ⓒ 로이터=뉴스1 (서울=뉴스1) 신기림 기자 =", + "body": "선박들이 지난 4월 22일 오만 무산담 인근 호르무즈 해협을 지나고 있다. 2026.05.19. ⓒ 로이터=뉴스1 (서울=뉴스1) 신기림 기자 =\n\n국제유가가 도널드 트럼프 미국 대통령의 이란 협상 낙관론에 급락했다. 미국 서부텍사스산원유(WTI)는 다시 배럴당 100달러 아래로 내려왔다.\n\n20일(현지시간) 뉴욕상업거래소에서 WTI 선물은 전장보다 5.66% 급락한 배럴당 98.26달러에 거래를 마쳤다. 글로벌 기준유인 브렌트유 선물도 5.63% 하락한 105.02달러에 마감했다.\n\n이날 유가 급락은 트럼프 대통령의 발언이 촉발했다.\n\n트럼프 대통령은 이날 기자들에게 미국과 이란의 협상이 \"마지막 단계(final stages)\"에 접어들었다고 말했다고 CNBC가 보도했다.\n\n그는 이번 주 초에도 걸프 지역 동맹국들의 요청에 따라 이란에 대한 추가 군사 공격 계획을 보류하고 외교에 더 많은 시간을 주기로 했다고 밝힌 바 있다.\n\n다만 시장에서는 트럼프 대통령의 낙관론을 완전히 신뢰하지는 않는 분위기다.\n\n트럼프 대통령은 그동안에도 이란과의 조기 합의 가능성을 반복적으로 언급했지만 이후 미·이란 긴장이 다시 고조되는 일이 반복됐다.\n\n현재 이란은 호르무즈 해협 봉쇄를 유지하고 있으며 미국도 이란 항만 봉쇄를 이어가고 있다. 호르무즈 해협은 전 세계 원유와 천연가스 물동량의 핵심 통로로 꼽힌다.\n\n공급 차질 장기화 가능성에 대한 우려도 여전하다.\n\n씨티그룹은 전날 보고서에서 시장이 호르무즈 원유 공급 차질 장기화 위험을 과소평가하고 있다고 경고했다. 씨티는 브렌트유 가격이 단기적으로 배럴당 120달러까지 오를 수 있다고 전망했다.\n\n씨티 애널리스트들은 고객 메모에서 \"이란 정권이 상당 기간 호르무즈 해협 물류 흐름을 방해할 가능성이 점점 커지고 있다\"고 분석했다.\n\n에너지 컨설팅업체 우드맥켄지도 이날 분석 보고서에서 최악의 경우 호르무즈 해협 봉쇄가 연말까지 이어질 경우 국제유가가 배럴당 200달러에 근접할 수 있다고 전망했다.\n\n반면 미국과 이란이 조기에 평화 합의에 도달해 6월 중 호르무즈 해협이 정상화될 경우 브렌트유 현물 가격은 2026년 말 배럴당 80달러 수준까지 내려갈 수 있다고 우드맥킨지는 내다봤다.", + "htmlBody": "
\"선박들이

선박들이 지난 4월 22일 오만 무산담 인근 호르무즈 해협을 지나고 있다. 2026.05.19. ⓒ 로이터=뉴스1 (서울=뉴스1) 신기림 기자 =

선박들이 지난 4월 22일 오만 무산담 인근 호르무즈 해협을 지나고 있다. 2026.05.19. ⓒ 로이터=뉴스1 (서울=뉴스1) 신기림 기자 =

국제유가가 도널드 트럼프 미국 대통령의 이란 협상 낙관론에 급락했다. 미국 서부텍사스산원유(WTI)는 다시 배럴당 100달러 아래로 내려왔다.

20일(현지시간) 뉴욕상업거래소에서 WTI 선물은 전장보다 5.66% 급락한 배럴당 98.26달러에 거래를 마쳤다. 글로벌 기준유인 브렌트유 선물도 5.63% 하락한 105.02달러에 마감했다.

이날 유가 급락은 트럼프 대통령의 발언이 촉발했다.

트럼프 대통령은 이날 기자들에게 미국과 이란의 협상이 "마지막 단계(final stages)"에 접어들었다고 말했다고 CNBC가 보도했다.

그는 이번 주 초에도 걸프 지역 동맹국들의 요청에 따라 이란에 대한 추가 군사 공격 계획을 보류하고 외교에 더 많은 시간을 주기로 했다고 밝힌 바 있다.

다만 시장에서는 트럼프 대통령의 낙관론을 완전히 신뢰하지는 않는 분위기다.

트럼프 대통령은 그동안에도 이란과의 조기 합의 가능성을 반복적으로 언급했지만 이후 미·이란 긴장이 다시 고조되는 일이 반복됐다.

현재 이란은 호르무즈 해협 봉쇄를 유지하고 있으며 미국도 이란 항만 봉쇄를 이어가고 있다. 호르무즈 해협은 전 세계 원유와 천연가스 물동량의 핵심 통로로 꼽힌다.

공급 차질 장기화 가능성에 대한 우려도 여전하다.

씨티그룹은 전날 보고서에서 시장이 호르무즈 원유 공급 차질 장기화 위험을 과소평가하고 있다고 경고했다. 씨티는 브렌트유 가격이 단기적으로 배럴당 120달러까지 오를 수 있다고 전망했다.

씨티 애널리스트들은 고객 메모에서 "이란 정권이 상당 기간 호르무즈 해협 물류 흐름을 방해할 가능성이 점점 커지고 있다"고 분석했다.

에너지 컨설팅업체 우드맥켄지도 이날 분석 보고서에서 최악의 경우 호르무즈 해협 봉쇄가 연말까지 이어질 경우 국제유가가 배럴당 200달러에 근접할 수 있다고 전망했다.

반면 미국과 이란이 조기에 평화 합의에 도달해 6월 중 호르무즈 해협이 정상화될 경우 브렌트유 현물 가격은 2026년 말 배럴당 80달러 수준까지 내려갈 수 있다고 우드맥킨지는 내다봤다.

", + "tags": [ + "네이버뉴스", + "생활/문화" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-20T20:23:18.000Z" + }, + "createdAt": "2026-05-26T00:11:25.810Z", + "updatedAt": "2026-05-26T00:11:25.810Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-82505965fb7e99b0", + "title": "`탱크데이` 파문 나흘째…스타벅스 사실상 업무 올스톱", + "sourceLabel": "이데일리", + "url": "https://n.news.naver.com/mnews/article/018/0006287220", + "lead": "대표 해임 초강수 조치에도 후폭풍 마케팅·프로모션 줄줄이 중단·연기 대표 행사 ‘서머 e-프리퀀시’도 재검토 탈퇴·불매 움직임에 직원 부담 가중 신세계그룹 차원 대응 관리체계 강화 [이데일리 김미경 기자] 5·18민주화운동 기념일에 불거진 이른바 ‘탱크데이’ 마케팅 논란이 나흘째 이어지면서 스타벅스코리아 내부 업무가 사실상 올스톱 상태에 빠졌다. 대표이사 해임이라는 초강수 조치 이후에도 후폭풍이 거세자 주요 사업과 마케팅 일정을 줄줄이 중단·연기하는 상황이다.", + "body": "대표 해임 초강수 조치에도 후폭풍 마케팅·프로모션 줄줄이 중단·연기 대표 행사 ‘서머 e-프리퀀시’도 재검토 탈퇴·불매 움직임에 직원 부담 가중 신세계그룹 차원 대응 관리체계 강화 [이데일리 김미경 기자] 5·18민주화운동 기념일에 불거진 이른바 ‘탱크데이’ 마케팅 논란이 나흘째 이어지면서 스타벅스코리아 내부 업무가 사실상 올스톱 상태에 빠졌다. 대표이사 해임이라는 초강수 조치 이후에도 후폭풍이 거세자 주요 사업과 마케팅 일정을 줄줄이 중단·연기하는 상황이다.\n\n21일 관련 업계에 따르면 스타벅스코리아는 현재 최종 의사결정권자인 대표이사가 공석인 상태다. 논란이 불거진 지난 18일 대표이사가 전격 해임된 이후 주요 현안에 대한 판단과 집행이 사실상 지연되고 있다.\n\n현재 스타벅스코리아는 별도의 대표이사 직무대행 체제를 두지 않고 있는 것으로 파악됐다. 대신 각 본부장과 담당 임원들이 주요 사안을 협의하고, 상황의 엄중함을 고려해 신세계그룹과 긴밀히 소통하며 대응 중인 것으로 알려졌다.\n\n스타벅스가 5·18 민주화운동과 1987년 박종철 열사 고문치사 사건을 비하하는 이벤트로 논란이 된 가운데 20일 서울 시내 한 스타벅스 모습. (사진=이데일리 이영훈 기자).\n\n스타벅스 내부에서는 대표이사 경질 이후에도 논란이 좀처럼 가라앉지 않자 직원들의 동요도 커지는 분위기다. 온라인상에서는 탈퇴·불매 움직임으로 번지는 양상인 데다, 정치권과 시민사회를 중심으로 비판 여론이 이어지면서 내부 분위기 역시 뒤숭숭한 것으로 알려졌다. 조직 내에서는 “현재 어떤 결정도 쉽게 내리기 어려운 상황”이라는 분위기도 감지된다.\n\n이에 따라 스타벅스토리아는 핵심 마케팅과 프로모션 일정을 잇따라 중단하거나 연기하고 있다. 스타벅스코리아는 전날 내부망 공지를 통해 “무거운 책임감과 자숙의 마음으로 각종 행사와 프로모션을 연기 또는 취소하고 있다”고 밝혔다. 특히 매년 대규모 고객 참여를 이끌어온 ‘서머 e-프리퀀시’를 비롯한 여름 시즌 프로모션 전반이 재검토에 들어가면서 현업 부서의 부담도 커지고 있는 것으로 알려졌다.\n\n업계에서는 스타벅스가 여름 시즌 굿즈 마케팅과 프로모션을 통해 고객 유입 효과를 극대화해온 만큼, 이번 일정 차질이 실적과 브랜드 이미지 모두에 적지 않은 부담으로 작용할 수 있다는 관측이 나온다.\n\n신세계그룹 차원의 관리도 강화되는 분위기다. 앞서 신세계그룹 컨트롤타워인 경영전략실의 이규봉 경영지원총괄은 지난 19일 스타벅스코리아를 찾아 임원회의를 주재한 것으로 전해졌다. 당시 회의에서는 조직 구성원들에게 동요하지 말고 기존 업무 체계 유지에 집중해달라는 당부가 나온 것으로 알려졌다.\n\n다만 후임 인선과 경영 정상화 시점은 불투명하다. 그룹 차원의 인사 발표 시점이 정해지지 않은 가운데 당분간은 본부장·임원 협의 체제가 이어질 가능성이 거론된다. 업계에서는 이번 논란이 단순 마케팅 이슈를 넘어 조직 운영 전반에까지 영향을 미치고 있다는 분석도 나온다.\n\n스타벅스 관계자는 “현재 관련 사안에 대해 조사를 진행 중이며 재발 방지를 위한 내부 시스템을 점검하고 있는 상황”이라며 “상황이 엄중한 만큼 그룹과 긴밀히 소통하고 있다”고 말했다.\n\n앞서 스타벅스는 지난 18일 텀블러 세트 판매 프로모션 과정에서 ‘책상에 탁!’ ‘5·18 탱크데이’ 등의 문구를 사용해 논란에 휩싸였다. 이를 두고 박종철 고문치사 사건 당시 경찰의 “책상을 탁 치니 억 하고 죽었다”는 발언과 5·18 당시 계엄군 장갑차를 연상시킨다는 지적이 나오면서 “5·18 민주화운동을 희화화·폄훼한 것 아니냐”는 비판이 확산됐다.\n\n스타벅스가 5·18 민주화운동과 1987년 박종철 열사 고문치사 사건을 비하하는 이벤트로 논란이 된 가운데 20일 서울 한 스타벅스에 사과문이 붙어 있다. (사진=이데일리 이영훈 기자).", + "htmlBody": "

대표 해임 초강수 조치에도 후폭풍 마케팅·프로모션 줄줄이 중단·연기 대표 행사 ‘서머 e-프리퀀시’도 재검토 탈퇴·불매 움직임에 직원 부담 가중 신세계그룹 차원 대응 관리체계 강화 [이데일리 김미경 기자] 5·18민주화운동 기념일에 불거진 이른바 ‘탱크데이’ 마케팅 논란이 나흘째 이어지면서 스타벅스코리아 내부 업무가 사실상 올스톱 상태에 빠졌다. 대표이사 해임이라는 초강수 조치 이후에도 후폭풍이 거세자 주요 사업과 마케팅 일정을 줄줄이 중단·연기하는 상황이다.

21일 관련 업계에 따르면 스타벅스코리아는 현재 최종 의사결정권자인 대표이사가 공석인 상태다. 논란이 불거진 지난 18일 대표이사가 전격 해임된 이후 주요 현안에 대한 판단과 집행이 사실상 지연되고 있다.

현재 스타벅스코리아는 별도의 대표이사 직무대행 체제를 두지 않고 있는 것으로 파악됐다. 대신 각 본부장과 담당 임원들이 주요 사안을 협의하고, 상황의 엄중함을 고려해 신세계그룹과 긴밀히 소통하며 대응 중인 것으로 알려졌다.

\"스타벅스가
스타벅스가 5·18 민주화운동과 1987년 박종철 열사 고문치사 사건을 비하하는 이벤트로 논란이 된 가운데 20일 서울 시내 한 스타벅스 모습. (사진=이데일리 이영훈 기자).

스타벅스 내부에서는 대표이사 경질 이후에도 논란이 좀처럼 가라앉지 않자 직원들의 동요도 커지는 분위기다. 온라인상에서는 탈퇴·불매 움직임으로 번지는 양상인 데다, 정치권과 시민사회를 중심으로 비판 여론이 이어지면서 내부 분위기 역시 뒤숭숭한 것으로 알려졌다. 조직 내에서는 “현재 어떤 결정도 쉽게 내리기 어려운 상황”이라는 분위기도 감지된다.

이에 따라 스타벅스토리아는 핵심 마케팅과 프로모션 일정을 잇따라 중단하거나 연기하고 있다. 스타벅스코리아는 전날 내부망 공지를 통해 “무거운 책임감과 자숙의 마음으로 각종 행사와 프로모션을 연기 또는 취소하고 있다”고 밝혔다. 특히 매년 대규모 고객 참여를 이끌어온 ‘서머 e-프리퀀시’를 비롯한 여름 시즌 프로모션 전반이 재검토에 들어가면서 현업 부서의 부담도 커지고 있는 것으로 알려졌다.

업계에서는 스타벅스가 여름 시즌 굿즈 마케팅과 프로모션을 통해 고객 유입 효과를 극대화해온 만큼, 이번 일정 차질이 실적과 브랜드 이미지 모두에 적지 않은 부담으로 작용할 수 있다는 관측이 나온다.

신세계그룹 차원의 관리도 강화되는 분위기다. 앞서 신세계그룹 컨트롤타워인 경영전략실의 이규봉 경영지원총괄은 지난 19일 스타벅스코리아를 찾아 임원회의를 주재한 것으로 전해졌다. 당시 회의에서는 조직 구성원들에게 동요하지 말고 기존 업무 체계 유지에 집중해달라는 당부가 나온 것으로 알려졌다.

다만 후임 인선과 경영 정상화 시점은 불투명하다. 그룹 차원의 인사 발표 시점이 정해지지 않은 가운데 당분간은 본부장·임원 협의 체제가 이어질 가능성이 거론된다. 업계에서는 이번 논란이 단순 마케팅 이슈를 넘어 조직 운영 전반에까지 영향을 미치고 있다는 분석도 나온다.

스타벅스 관계자는 “현재 관련 사안에 대해 조사를 진행 중이며 재발 방지를 위한 내부 시스템을 점검하고 있는 상황”이라며 “상황이 엄중한 만큼 그룹과 긴밀히 소통하고 있다”고 말했다.

앞서 스타벅스는 지난 18일 텀블러 세트 판매 프로모션 과정에서 ‘책상에 탁!’ ‘5·18 탱크데이’ 등의 문구를 사용해 논란에 휩싸였다. 이를 두고 박종철 고문치사 사건 당시 경찰의 “책상을 탁 치니 억 하고 죽었다”는 발언과 5·18 당시 계엄군 장갑차를 연상시킨다는 지적이 나오면서 “5·18 민주화운동을 희화화·폄훼한 것 아니냐”는 비판이 확산됐다.

\"스타벅스가
스타벅스가 5·18 민주화운동과 1987년 박종철 열사 고문치사 사건을 비하하는 이벤트로 논란이 된 가운데 20일 서울 한 스타벅스에 사과문이 붙어 있다. (사진=이데일리 이영훈 기자).
", + "tags": [ + "네이버뉴스", + "경제" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T06:22:19.000Z" + }, + "createdAt": "2026-05-26T00:11:25.226Z", + "updatedAt": "2026-05-26T00:11:25.226Z" + }, + { + "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", + "article": { + "id": "naver-news-159a010a06e2c336", + "title": "중소기업계 \"삼성노사 합의 환영…협력업체도 정당한 보상 필요\"", + "sourceLabel": "아시아경제", + "url": "https://n.news.naver.com/mnews/article/277/0005766372", + "lead": "\"대·중소기업 간 임금 격차 벌어질 우려\" 중소기업계가 삼성전자와 공동교섭단 노동조합이 잠정 합의안에 공식 서명한 것에 대해 환영한다는 입장을 밝혔다. 다만, 이번 협상으로 대기업과 중소기업 간 임금 및 복리후생 격차는 더 커질 수 있다고 우려했다.", + "body": "\"대·중소기업 간 임금 격차 벌어질 우려\" 중소기업계가 삼성전자와 공동교섭단 노동조합이 잠정 합의안에 공식 서명한 것에 대해 환영한다는 입장을 밝혔다. 다만, 이번 협상으로 대기업과 중소기업 간 임금 및 복리후생 격차는 더 커질 수 있다고 우려했다.\n\n20일 경기도 수원시 장안구 경기고용노동청에서 열린 삼성전자 임금협상을 마친 후 여명구 삼성전자 DS(디바이스솔루션·반도체 사업 담당) 피플팀장과 최승호 삼성그룹 초기업노동조합 삼성전자지부 위원장이 잠정 합의안에 서명한 후 손을 맞잡고 있다. 연합뉴스\n\n21일 중소기업중앙회는 공식 입장문을 통해 \"삼성전자 노사가 반도체 라인이 멈추는 극한의 사태까지 가지 않고 협상을 타결한 것에 대해 다행스럽게 생각한다\"며 \"이번 협상 타결은 글로벌 반도체 경쟁이 심화하는 시기에 우리 경제에 막대한 영향력을 가진 기업의 생산 차질 우려가 불식됐다는 점에서 의미가 있다\"고 밝혔다.\n\n중기중앙회는 \"다만, 삼성전자 노사협상 과정을 지켜본 중소기업 근로자와 사업주는 마음이 무겁다\"며 \"수억 원에 달하는 성과급 논쟁 속에서 과연 협력 중소기업들에는 정당한 대가와 보상이 이뤄졌는지 의문이 남는다. 중소기업 근로자의 임금은 대기업의 절반 수준이고, 각종 상여금과 복리후생의 격차는 더욱 크다\"고 지적했다.\n\n그러면서 \"우리나라 반도체 산업의 경쟁력은 수천개의 협력업체와 소재·부품 중소기업이 원팀으로 함께 일궈낸 성과\"라며 \"중소기업계는 삼성전자가 약속한 동반성장 대책이 협력업체의 연구 개발과 시설 투자, 임금 인상의 선순환 구조로 이어지는 실효성 있는 대책이 되길 바란다\"고 강조했다.", + "htmlBody": "

"대·중소기업 간 임금 격차 벌어질 우려" 중소기업계가 삼성전자와 공동교섭단 노동조합이 잠정 합의안에 공식 서명한 것에 대해 환영한다는 입장을 밝혔다. 다만, 이번 협상으로 대기업과 중소기업 간 임금 및 복리후생 격차는 더 커질 수 있다고 우려했다.

\"20일

20일 경기도 수원시 장안구 경기고용노동청에서 열린 삼성전자 임금협상을 마친 후 여명구 삼성전자 DS(디바이스솔루션·반도체 사업 담당) 피플팀장과 최승호 삼성그룹 초기업노동조합 삼성전자지부 위원장이 잠정 합의안에 서명한 후 손을 맞잡고 있다. 연합뉴스

21일 중소기업중앙회는 공식 입장문을 통해 "삼성전자 노사가 반도체 라인이 멈추는 극한의 사태까지 가지 않고 협상을 타결한 것에 대해 다행스럽게 생각한다"며 "이번 협상 타결은 글로벌 반도체 경쟁이 심화하는 시기에 우리 경제에 막대한 영향력을 가진 기업의 생산 차질 우려가 불식됐다는 점에서 의미가 있다"고 밝혔다.

중기중앙회는 "다만, 삼성전자 노사협상 과정을 지켜본 중소기업 근로자와 사업주는 마음이 무겁다"며 "수억 원에 달하는 성과급 논쟁 속에서 과연 협력 중소기업들에는 정당한 대가와 보상이 이뤄졌는지 의문이 남는다. 중소기업 근로자의 임금은 대기업의 절반 수준이고, 각종 상여금과 복리후생의 격차는 더욱 크다"고 지적했다.

그러면서 "우리나라 반도체 산업의 경쟁력은 수천개의 협력업체와 소재·부품 중소기업이 원팀으로 함께 일궈낸 성과"라며 "중소기업계는 삼성전자가 약속한 동반성장 대책이 협력업체의 연구 개발과 시설 투자, 임금 인상의 선순환 구조로 이어지는 실효성 있는 대책이 되길 바란다"고 강조했다.

", + "tags": [ + "네이버뉴스", + "경제" + ], + "listedDate": "2026-05-21", + "publishedAt": "2026-05-21T06:21:46.000Z" + }, + "createdAt": "2026-05-26T00:11:24.753Z", + "updatedAt": "2026-05-26T00:11:24.753Z" + }, { "ownerKey": "token:55da11db2e67390bf2bacb861407e697b7e4df64bfb6902d365b4f5856ed084e", "article": { @@ -695,7 +1175,7 @@ "url": "https://n.news.naver.com/mnews/article/243/0000098199", "lead": "이코노미스트 언론사 구독되었습니다. 메인 뉴스판에서 주요뉴스를 볼 수 있습니다. 보러가기 닫기", "body": "이코노미스트 언론사 구독되었습니다. 메인 뉴스판에서 주요뉴스를 볼 수 있습니다. 보러가기 닫기\n\n이코노미스트 언론사 구독 해지되었습니다. 닫기\n\n[속보] \"美와 합의시 이란 통제하에 호르무즈 선박수 회복\"\n\n이동 통신망을 이용하여 음성을 재생하면 별도의 데이터 통화료가 부과될 수 있습니다.\n\n[속보] \"美와 합의시 이란 통제하에 호르무즈 선박수 회복\"\n\n[속보] \"美와 합의시 이란 통제하에 호르무즈 선박수 회복\"\n\n김도정(dojeong@edaily.co.kr)\n\nCopyright ⓒ 이코노미스트. All rights reserved. 무단 전재 및 재배포 금지.\n\n이 기사는 언론사에서 경제 섹션으로 분류했습니다.\n\n기사 섹션 분류 안내 기사의 섹션 정보는 해당 언론사의 분류를 따르고 있습니다. 언론사는 개별 기사를 2개 이상 섹션으로 중복 분류할 수 있습니다.\n\n이코노미스트 구독하고 메인에서 바로 만나보세요! 구독하고 메인에서 만나보세요!\n\n언론사의 주요 뉴스를 메인에서 바로 만나보세요! 주요 뉴스를 메인에서 만나보세요!\n\n주요뉴스 해당 언론사에서 선정하며 언론사(아웃링크) 로 이동합니다.\n\n\"1년 만에 222% 수직상승\"…개미들만 몰랐던 시총 3위 거인의 소리 없는 독주\n\n금융 규제 비웃는 '스타벅스', 고객 돈 4,200억 굴리는데 공시 의무 제로\n\n942만 원 vs 176만 원…'반도체 대호황'이 세운 잔인한 임금 장벽\n\n마이크론 38조·TSMC 85조 쏟아붓는데…K-반도체는 '65조 성과급 딜레마'\n\n중국차 10배 더 팔려...한국인 푹 빠졌다\n\n국내 정상의 온라인·오프라인 경제지 이코노미스트\n\n전쟁 한파 끝나고 '코스피의 봄' 오나…코스피 5700선 회복\n\n\"달러, 영원한 기축통화 아닐 수도\"…월가 전설이 주목한 '비트코인'\n\n트럼프 만난 김민석 \"한반도 문제 해결할 유일한 리더라 얘기했다\"\n\n기름값 폭등에 칼 뽑은 독일…\"주유소 가격 인상 하루 1번만\"\n\n금값, 전쟁 속에서도 방향 잃었다…달러 강세에 귀금속 시장 '출렁'\n\n기사 추천은 24시간 내 50회까지 참여할 수 있습니다.\n\n이코노미스트 가 이 기사의 댓글 정책을 결정합니다.\n\n안내 댓글 정책 언론사별 선택제 섹션별로 기사의 댓글 제공여부와 정렬방식을 언론사가 직접 결정합니다. 기사 섹션 정보가 정치/선거를 포함하는 경우 정치/선거섹션 정책이 적용됩니다. 언론사의 결정에 따라 동일한 섹션이라도 기사 단위로 댓글 제공여부와 정렬 방식이 달라질 수 있습니다. 단, 일부 댓글 운영 방식 및 운영규정에 따른 삭제나 이용제한 조치는 네이버가 직접 수행합니다. 레이어 닫기\n\n[속보] 이란 관영매체, '서로 공격자제' 미국과 합의안 내용 언급\n\n\"1년 만에 222% 수직상승\"…개미들만 몰랐던 시총 3위 거인의 소리 없는 독주\n\n중국차 10배 더 팔려...한국인 푹 빠졌다\n\n마이크론 38조·TSMC 85조 쏟아붓는데…K-반도체는 '65조 성과급 딜레마'\n\n‘탱크 데이’ 후폭풍… 정부, 스타벅스 '총리 표창 취소' 검토 \"수시 취소 가능\"\n\n금융 규제 비웃는 '스타벅스', 고객 돈 4,200억 굴리는데 공시 의무 제로\n\n네이버 AI 뉴스 알고리즘 뉴스 추천 알고리즘이 궁금하다면?\n\n“탕탕탕” 총성 울리자 美 백악관 보안 요원들 “나가 나가 나가” [현장영상]\n\n트럼프, 이란 지도에 美성조기 합성…휴전 협상에도 '도발'\n\n“미-이란, 60일 휴전연장…호르무즈 무료 개방 MOU 근접” [악시오스]\n\n[AI는 지금] 中, AI 연인 규제 칼 빼들었다…\"미성년자 가상 연애 금지\"\n\n\"전통 검색 엔진 죽지 않을 것...AI와 혼용하는 '하이브리드' 모델 부상\"\n\nGC녹십자, 'AI 혈우병 관절병증 예측 시스템' 만든다\n\n이란 관영매체, '서로 공격자제' 미국과 합의안 내용 언급\n\n[속보] 이란 타스님 \"합의 시 호르무즈 선박 수, 30일 내 전쟁 이전으로\"\n\n이란 관영매체 \"미국과 서로 공격 자제하기로\"\n\n강남3구 가격 꿈틀… 정부 공언대로 '매물잠김' 풀어낼까 [주말 Q&A]\n\n불교 주요 사찰 모두 찾은 李대통령…“자타불이, 가장 필요한 가르침”\n\n李 대통령 “‘일베’ 등 혐오 방치 사이트 폐쇄 검토” [현장영상]\n\n불교 주요 사찰 모두 찾은 李대통령…“자타불이, 가장 필요한 가르침”\n\n봉하 '손가락 인증' 논란에…대통령 \"일베 폐쇄 검토\"\n\n李대통령 \"각자도생 아닌 공존상생 절실\"…부처님오신날 봉축\n\n이원택 전북지사 후보 \"농어촌기본소득 확대하겠다\"\n\n기초단체 잇단 경고등에 정청래 또 호남행…무소속·조국혁신당 접전 확대\n\n박수현 캠프, 장동혁 고발…與 \"용서할 수 없는 일\"\n\n이란 관영매체, '서로 공격자제' 미국과 합의안 내용 언급\n\n이란 관영매체 \"미국과 서로 공격 자제하기로\"\n\n이란 파르스 통신 “美·이란, 상호 공격 자제…평화 양해 각서에 담겨”\n\n법무부 출입국·외국인정책본부장 차관급 격상 [김태훈의 의미 또는 재미]\n\n[이충재 칼럼] 지금이 '민주 적통' 논쟁할 땐가\n\n이재명 대통령의 SNS는 칼...수술 도구로만 쓰자\n\n이원택 전북지사 후보 \"농어촌기본소득 확대하겠다\"\n\n기초단체 잇단 경고등에 정청래 또 호남행…무소속·조국혁신당 접전 확대\n\n\"불심을 잡아라\"... 부처님오신날 동화사 찾은 김부겸·추경호\n\n불교 주요 사찰 모두 찾은 李대통령…“자타불이, 가장 필요한 가르침”\n\n李대통령 \"미움 오직 자비로써 사라져…부처님말씀 등불로 '국민 목숨 살리는 정부' 최선\"\n\n李 “화합하라는 부처님의 가르침…국민 삶 세심히 살필 것”\n\n이 기사를 본 이용자들이 함께 많이 본 기사, 해당 기사와 유사한 기사, 관심 기사 등을 자동 추천합니다\n\n‘탱크데이’ 정용진, ‘개인정보 유출’ CJ… 굵직한 사건 몰리는데 내부 기강은 ‘흔들’ [채민석의 경솔한이야기]\n\n도살 직전 구조된 퇴역 경주마의 아들, 제주서 ‘새 삶’ 산다\n\n한동훈 41.5%-하정우 34.5%-박민식 18.9%…요동치는 부산 북갑\n\n\"평생 반성하겠다\"더니… 무기징역 선고받자 \"듣기 싫다\" 고함친 피고인 [사건 플러스]\n\n이준석, 李대통령 “일베 폐쇄”에… “정치가 제 역할 하면 설 자리 잃어”\n\n쌈싸먹으려 꺼냈는데 축 처진 상추···‘심폐소생수’는 얼음물일까, 온수일까\n\n마사지업소 성폭행, 피해자 사망…가해자는 10여명 성매매 장면 도촬 범행도\n\n남양주 철마산 실종 50대 외국인, 서울 집에서 발견\n\n\"부장님, 우리 호텔 갈까요?\"…2030 직장인들 몰린 이유[럭셔리월드]\n\n한국 잠수함 최초로 태평양 건넜다···도산안창호함, 최장 항해 기록 찍고 캐나다로 간 까닭\n\n사우나 안 '쿨쿨'…인형뽑기방 아수라장 20대 남성 체포\n\n\"노무현 추도식 날 '일베 손가락' 남녀, 봉하 마을 활보\"…조수진 변호사 '분노'\n\n“스벅가기 눈치 보여요” 생일 때 받은 기프티콘 어쩌나 [세상&]\n\n로또 1225회 당첨번호 22억, '1등 터지고 2등 없는 곳 보니'\n\n[단독] '철마산 실종' 홍콩 여성, 수색 사흘 만에 서울 강북구서 무사 발견\n\n유시민 \"평택을, 조국 당선이 낫다\" 박지현 \"공정함 무너뜨려\"\n\n“요즘 부자들은 손톱관리 안해”…맨손톱이 ‘부의 상징’이라는데\n\n마이크론 38조·TSMC 85조 쏟아붓는데…K-반도체는 '65조 성과급 딜레마'\n\n중국차 10배 더 팔려...한국인 푹 빠졌다\n\n‘탱크 데이’ 후폭풍… 정부, 스타벅스 '총리 표창 취소' 검토 \"수시 취소 가능\"\n\n금융 규제 비웃는 '스타벅스', 고객 돈 4,200억 굴리는데 공시 의무 제로\n\n[속보] 이란 관영매체, '서로 공격자제' 미국과 합의안 내용 언급\n\n[속보] \"미-이란, 60일 휴전연장·호르무즈 개방 합의 근접\"<美매체>\n\n“15살 어린 고교생과…” CCTV 아내 모습 경악\n\n‘9kg 감량’ 이시영 “삶은 계란 질릴 때 강추”…운동 후 먹는 ‘이 빵’ 정체?\n\n경찰, '탱크데이' 정용진 피의자 입건...휴일에도 수사 속도\n\n직장인 부업으로 번지는 '사이버 포주'…매뉴얼까지 판매 중\n\n전한길, 스타벅스 짠 짠 짠 “자유의 맛! 건배 한번 합시다~”\n\n\"10억 하던 아파트가 2년 만에\"…호재 줄줄이 집값 '들썩' [철길옆집]\n\n'스타벅스 인증' 논란 배우 정민찬 180도 달라진 사과문\n\n'5·18 탱크데이' 여파…법원에 스타벅스 선불금 지급명령 신청\n\n\"더 이상 뭉개지 마라\"…박형준 측, 전재수 후보에 '최후통첩'\n\n“탈모 막으려 먹었는데”…‘이 영양제’로 암 발견 늦어질 수도\n\n“아저씨는 입지 마세요, 징그러우니까” 도쿄도청 반바지 출근이 소환한 ‘아재혐오’[이세계도쿄]\n\n[속보]부처님오신날 사찰 찾은 80대 남성·70대 여성 탄 승용차, 계곡 추락\n\n스타벅스 '5·18 폄훼 논란' 후폭풍…미사용 선불금 지급명령 신청\n\n\"1년 만에 222% 수직상승\"…개미들만 몰랐던 시총 3위 거인의 소리 없는 독주\n\n‘탱크 데이’ 후폭풍… 정부, 스타벅스 '총리 표창 취소' 검토 \"수시 취소 가능\"\n\n942만 원 vs 176만 원…'반도체 대호황'이 세운 잔인한 임금 장벽\n\n日 소도시는 뚫렸는데…韓 지방은 동맥경화 [K관광, ‘서울 독식’을 깨라]②\n\n마이크론 38조·TSMC 85조 쏟아붓는데…K-반도체는 '65조 성과급 딜레마'\n\n금융 규제 비웃는 '스타벅스', 고객 돈 4,200억 굴리는데 공시 의무 제로\n\n기사배열 책임자 : 김수향 청소년 보호 책임자 : 이정규\n\n각 언론사가 직접 콘텐츠를 편집합니다. ⓒ 이코노미스트\n\n이 콘텐츠의 저작권은 저작권자 또는 제공처에 있으며, 이를 무단 이용하는 경우 저작권법 등에 따라 법적 책임을 질 수 있습니다.", - "htmlBody": "

var svt = "20260524165614.743"; var timestamp = svt.substr(0, 8); var isLogin = false; var uhv = "";

var service = { news: false, entertain: false, sports: false, mnews: true, newsType: true }; var serviceName = "mnews";

var nilNtmUrl = "https://ntm.pstatic.net/scripts/ntm_b7032129a433.js";

var envPhase = "production"; var isProduction = true;

var gnb_service = "news"; var gnb_logout = encodeURIComponent(location.href); var gnb_template = "gnb_utf8"; var gnb_brightness = 1; var gnb_item_hide_option = 128;

var isDegradeMode = false; var isDegradeN2Mode = false;

\"이코노미스트\"

이코노미스트 언론사 구독되었습니다.\n메인 뉴스판에서 주요뉴스를\n볼 수 있습니다.\n보러가기\n닫기

이코노미스트 언론사 구독 해지되었습니다.\n닫기

[속보] "美와 합의시 이란 통제하에 호르무즈 선박수 회복"

입력 2026.05.24. 오후 4:54

텍스트 음성 변환 서비스 사용하기

이동 통신망을 이용하여 음성을 재생하면 별도의 데이터 통화료가 부과될 수 있습니다.

\"[속보]

[속보] "美와 합의시 이란 통제하에 호르무즈 선박수 회복"

[속보] "美와 합의시 이란 통제하에 호르무즈 선박수 회복"

김도정(dojeong@edaily.co.kr)

Copyright ⓒ 이코노미스트. All rights reserved. 무단 전재 및 재배포 금지.

이 기사는 언론사에서 경제 섹션으로 분류했습니다.

기사의 섹션 정보는 해당 언론사의 분류를 따르고 있습니다. 언론사는 개별 기사를 2개 이상 섹션으로 중복 분류할 수 있습니다.

이코노미스트 구독하고 메인에서 바로 만나보세요! 구독하고 메인에서 만나보세요!

언론사의 주요 뉴스를 메인에서 바로 만나보세요! 주요 뉴스를 메인에서 만나보세요!

이코노미스트\n\n주요뉴스 해당 언론사에서 선정하며 언론사(아웃링크) 로 이동합니다.

이코노미스트 언론사가 직접 선정한 이슈

window.__htExternalUrl = window.__htExternalUrl || {}; window.__htExternalUrl["like"] = "https://static-like.pstatic.net/js/reaction/dist/reaction.min.js"; window.__htLikeOption = { type: "multi", cssId: "news", domain: "https://news.like.naver.com", staticDomain: "https://static-like.pstatic.net", isMobile: true, isHiddenLabel: false, isHiddenCount: false, isHiddenZeroCount: false, isUsedLabelAsZeroCount: false, isDebugMode: false, isDuplication: false, isHiddenLayerAfterSelection: true, clicklog: function(el, name) { nclkWrap.send({ $this: $(el), sArea: name }); } }; window.__htLikeOption["isZeroFace"] = true;

기사 추천은 24시간 내 50회까지 참여할 수 있습니다.

window.__htExternalUrl = window.__htExternalUrl || {}; window.__htExternalUrl["share"] = "https://ssl.pstatic.net/spi/js/release/ko_KR/splugin.js"; window.__htShareOption = { "scriptUrl": window.__htExternalUrl["share"], "evKey": "news", "dimmed": "fixed", "serviceName": "뉴스", "sourceName": "" }; if (isAvailableDarkMode) { window.__htShareOption["darkMode"] = true; }

window.__htExternalUrl = window.__htExternalUrl || {}; window.__htExternalUrl["tip"] = "https://ssl.pstatic.net/dicimg/tip/tip_m.js"; window.__htExternalUrl["pcTip"] = "https://ssl.pstatic.net/dicimg/tip/tip_pc.js";

window.__htExternalUrl["handOff"] = { "category": "news_article", "apiDomain": "https://sidekick.fever.naver.com" };

\"섬네일

스티커 하나만 모아도 네이버페이 행운이!

선거 특집페이지에서 출석 스티커 받고, 최대 15,000P의 행운을 기대해요!

window.__htExternalUrl = window.__htExternalUrl || {}; window.__htExternalUrl["comment"] = "https://ssl.pstatic.net/static.cbox/js/cbox.core.js"; window.__htCboxOption = {"htMessage":{"template":{"user":{"profile":{"help":{"last":{"help_desc":"최근 30일간 작성한 댓글의 정보를 요약해 보여줍니다. 작성대비 본인의 삭제비율 (본인삭제율) 과 작성글이 받은 피드백 중 공감비율 (받은공감률) 을 표기합니다. 이때, 받은공감률은 댓글 삭제로 변동된 정보를 반영하지 않습니다."}},"title":"뉴스 댓글모음","private_back":"기사로 돌아가기"},"header":{"help":{"content":"댓글목록을 공개한 사용자의 댓글모음이 제공됩니다. 나의 댓글 모음에서 다른 사람에게 공개 또는 비공개 설정을 할 수 있습니다. 제공되는 목록에 삭제댓글/답글은 포함하지 않습니다.","title":"뉴스댓글 도움말"}}},"social_login_message":"계정을 선택하시면 로그인&middot;계정인증 을 통해 댓글을 남기실 수 있습니다. 뉴스서비스에서는 소셜 계정 사용이 불가하며 댓글모음만 확인하실 수 있습니다.","write_placeholder":"댓글을 입력해주세요","sort_favorite":"순공감순","duplicate_caution_link_address_pc":"https://news.naver.com/main/principle.naver","duplicate_caution_link_address_mobile":"https://m.news.naver.com/ombudsman/index?mode=rule&open=comment","duplicate_caution_link_title":"운영규정 자세히 알아보기","duplicate_caution_cont":"반복 등록이 계속될 경우 운영규정에 따라 서비스 이용이 정지될 수 있습니다.","duplicate_caution_title":"동일 내용 댓글의 반복 등록이 확인되었습니다.","write_long_placeholder":"다양한 의견이 서로 존중될 수 있도록 다른 사람에게 불쾌감을 주는 욕설, 혐오, 비하의 표현이나 타인의 권리를 침해하는 내용은 주의해주세요. 모든 작성자는 본인이 작성한 의견에 대해 법적 책임을 갖는다는 점 유의하시기 바랍니다.","help_titles":["댓글이력공개안내","댓글노출정책"],"info_comment_off":" 이코노미스트 댓글 정책에 따라 이 기사에서는 댓글을 제공하지 않습니다.","userblock_completed_dialog_content":"댓글모음이나 기사 댓글에서 차단을 해제할 수 있습니다.","userblock_ready_dialog_content":"이제 기사 댓글이나 댓글모음에서 상대방의 댓글을 확인할 수 없습니다.","userblocked_message_content":"댓글모음이나 기사 댓글에서 차단한 이용자들을 관리할 수 있습니다.","userunblock_completed_dialog_content":"상대방의 댓글모음이나 기사 댓글에서 언제든지 차단할 수 있습니다.","userunblock_ready_dialog_content":"이제 기사 댓글이나 댓글모음에서 상대방의 댓글을 확인할 수 있습니다.","view_all":"댓글 더보기","help_contents":["작성자가 삭제한 댓글(작성자 삭제), 운영자가 삭제한 댓글(규정 미준수) 그리고 삭제되지 않은 현재 남은 댓글의 수와 이력을 투명하게 제공합니다.","순공감순은 공감수에서 비공감수를 뺀 수치가 많은 댓글입니다. 비정상적인 방법으로 공감수가 증가하는 경우 제외될 수 있습니다."],"reply_close":"답글 접기","list_empty":"등록된 댓글이 없습니다.","comment_link":"이 기사의 댓글 전체보기"},"stats":{"title":"댓글 작성 통계","view":"통계","fold":"접기","user_env_title":"어디에서 댓글을 썼을까요?"},"alert":{"max_length":"{0}자까지 입력할 수 있습니다.","login_require":"로그인을 하신 후 이용해 주시기 바랍니다.","min_length":"내용을 최소 {0}자 이상 입력해주세요."}},"nPageSize":5,"sObjectId":"news243,0000098199","sLikeItId":"ne_243_0000098199","sTicket":"news","nUserCommentReplyPageSize":10,"sAuthType":"naver","sTemplateId":"view_economy","htErrorHandler":{"3003":"고객님께서는 운영원칙에서 제한하고 있는 글을 작성하셨기 때문에 글쓰기를 포함한 일부 활동이 일정 기간 제한되었습니다. 동일 내용 댓글의 반복작성이 확인된 경우 제한기간은 1일이며, 반복이 계속되면 기간이 연장될 수 있습니다.","3004":"고객님께서는 운영원칙에서 제한하고 있는 글을 다수 작성하셨기 때문에 글쓰기를 포함한 일부​ 활동이 일정 기간 제한되었습니다.","3005":"고객님의 IP는 운영원칙에서 제한하고 있는 글을 작성하셨기 때문에 글쓰기를 포함한 일부​ 활동이 제한되었습니다.","3006":"고객님께서는 운영원칙에서 제한하고 있는 글을 작성하셨기 때문에 무기한 글쓰기를 포함한 일부​ 활동이 제한되었습니다.","3011":"이코노미스트 댓글 정책에 따라 이 기사에서는 댓글을 제공하지 않습니다.","4004":"해당되는 댓글이 없습니다.","5003":"정상적으로 반영되지 않았습니다. 화면 새로고침 후 다시 시도해 주세요.","5006":"자신의 글에 공감 할 수 없습니다.","5008":"자신의 글에 비공감 할 수 없습니다.","5010":"댓글/답글은 60초 내에 한 개만 등록하실 수 있습니다.","5026":"공감/비공감은 최근 24시간내 50회까지 가능합니다.","5027":"공감/비공감은 10초 내에 한 번만 클릭하실 수 있습니다.","5029":"기사당 댓글은 3개, 답글은 10개까지 작성할 수 있습니다.","5080":"실명확인을 받지 않은 아이디는 가입 이후 7일이 지난 시점부터 댓글 활동이 가능합니다.","5091":"본인 확인이 되지 않은 계정입니다. 본인 확인 후 다시 시도해주세요."},"bSortCookie":true,"bMobile":false,"bShowReply":false,"bManager":false,"sDomain":"https://ssl.pstatic.net/static.cbox","sCountry":"KR","nUserCommentPageSize":20,"sLanguage":"ko","sDateFormat":"Y.m.d. H:i:s","sCommentNo":"","sPageType":"more","sHelp":"down","sSort":"NEW","sApiDomain":"https://apis.naver.com/commentBox/cbox5","nFollowSize":5,"nFeedPageSize":50,"bIncludeAllComments":true,"sCountType":"comment","aFormation":["count","graph","write","notice","follow","list","view"],"bByCountry":false,"bUseAgo":false,"nReplyPageSize":20,"sCssId":"news_w","aSortTypes":["FAVORITE","NEW","RELATIVE"]}; window.__htCboxOption["bLogin"] = isLogin; window.__htCboxOption["bMobile"] = false; window.__htCboxOption["sHighlightColor"] = "rgba(124, 255, 182, 0.06)"; window.__htCboxOption["vViewAllHandler"] = function() { window.location.href = "/mnews/article/comment/243/0000098199"; };

이코노미스트 가 이 기사의 댓글 정책을 결정합니다.\n\n안내\n댓글 정책 언론사별 선택제\n섹션별로 기사의 댓글 제공여부와 정렬방식을 언론사가 직접 결정합니다. 기사 섹션 정보가 정치/선거를 포함하는 경우 정치/선거섹션 정책이 적용됩니다. 언론사의 결정에 따라 동일한 섹션이라도 기사 단위로 댓글 제공여부와 정렬 방식이 달라질 수 있습니다. 단, 일부 댓글 운영 방식 및 운영규정에 따른 삭제나 이용제한 조치는 네이버가 직접 수행합니다.\n레이어 닫기

오후 3시~4시까지 집계한 결과입니다.

\"기사

네이버 AI 뉴스 알고리즘 뉴스 추천 알고리즘이 궁금하다면?

언론사에서 직접 선별한 이슈입니다.

35개 언론사 52,918개 기사

12개 언론사 10,851개 기사

33개 언론사 25,820개 기사

27개 언론사 45,337개 기사

24개 언론사 31,751개 기사

18개 언론사 16,227개 기사

13개 언론사 27,114개 기사

이 기사를 본 이용자들이 함께 많이 본 기사, 해당 기사와 유사한 기사, 관심 기사 등을 자동 추천합니다

오후 1시~4시까지 집계한 결과입니다.

로그인\n\n전체서비스\n\n서비스안내\n\n오류신고\n\n고객센터\n\n기사배열 책임자 : 김수향\n청소년 보호 책임자 : 이정규\n\n각 언론사가 직접 콘텐츠를 편집합니다.\nⓒ 이코노미스트\n\n이 콘텐츠의 저작권은 저작권자 또는 제공처에 있으며, 이를 무단\n이용하는 경우 저작권법 등에 따라 법적 책임을 질 수 있습니다.\n\nNAVER

{{#series}} {{value}} 연재 구독을 취소하시겠습니까? {{/series}} {{#journalist}} {{value}} 기자의 기사 구독을 취소하시겠습니까? {{/journalist}}

{{officeName}} 구독이 완료되었습니다. 알림 으로 받아 보시겠습니까?

{{officeName}} 구독이 완료되었습니다. 알림 으로 받아 보시겠습니까?

네이버 앱 푸시알림이 꺼져있어요.

네이버 앱 알림켜기 > 알림탭 > 알림허용

버튼을 눌러 앱 알림 설정을 켜주세요.

{{#content.image}}

{{#content.image.class}}

{{/content.image.class}}

{{/content.image}} {{#isTextType}}

{{#content.image}}

{{#content.image.class}}

{{/content.image.class}}

{{/content.image}} {{#isTextType}} {{content.title}}

{{/isTextType}} {{#isTitleType}}

{{content.headTitle}} {{content.title}}

{{content.description}}

{{content.cancelMessage}}

{{content.confirmMessage}}

{{toastTitle}} {{toastMessage}}

{{officeName}} 언론사 알림이 설정되었습니다.{{#isSupportedNaverappNotificationSetting}} 네이버앱 전체 푸시 알림도 활성화된 상태입니다.{{/isSupportedNaverappNotificationSetting}}

window.ntm = window.ntm || []; var ntmOption = {};

var section = {"id":"101","name":"경제"}; var redirectSection = {"id":"101","name":"경제"};

ntmOption["channelId"] = article.officeId;

// 기타 또는 주요 섹션 이름 var nilSendSectionName = "기타"; var nilSendSectionId = ""; if (/100|101|102|103|104|105|106|107/.test(section.id)) { nilSendSectionName = section.name; nilSendSectionId = section.id; }

// 기사 타입 var htArticleTypeName = { "0": "text", "1": "photo", "2": "tv" }; var articleTypeName = htArticleTypeName[article.type] || "";

ntmOption["hitType"] = "cv"; ntmOption["eventCategory"] = "post_view"; ntmOption["uri"] = "http://news.naver.com/main/read.nhn?oid=243&aid=0000098199"; ntmOption["gdid"] = article.gdid; ntmOption["dimension1"] = encodeURIComponent(nilSendSectionName); ntmOption["dimension4"] = articleTypeName; ntmOption["optional2"] = nilSendSectionId;

var journalistIdList = [];

// 기자 목록 $("._JOURNALIST_CARD ._JOURNALIST_ID").each(function (index, object) { var $this = $(object); var journalistId = $this.attr("data-journalistId");

if (journalistId && journalistIdList.indexOf(journalistId) === -1) { journalistIdList.push(journalistId); } });

if (journalistIdList.length > 0) { ntmOption["dimension3"] = journalistIdList.join("___"); }

if (location.href.match(/ntype=RANKING/)) { ntmOption["dimension5"] = "y"; }

if (service.sports || service.newsType) { ntmOption["dimension2"] = article.sectionInfo.secondSection + "." + article.sectionInfo.thirdSection; }

var seriesComponentId = $("._VALID_SERIES").attr("data-component-id"); ntmOption["dimension6"] = seriesComponentId;

var issueComponentId = $("._ISSUE_ARTICLE_INFO").attr("data-component-id"); ntmOption["dimension8"] = issueComponentId;

if (/[?|&]type=breakingnews(&|$)/.test(location.href) === true) { if (/(m\\.naver\\.com)|(media\\.naver\\.com)|(news\\.naver\\.com\\/?$)|(www\\.naver\\.com)/.test(document.referrer) === true) { ntmOption["dimension7"] = "breaking"; } }

var ntmLeaveOption = $.extend({}, ntmOption); ntmLeaveOption["hitType"] = "event"; ntmLeaveOption["eventCategory"] = "action"; ntmLeaveOption["eventAction"] = "leave";

if (/\\/article\\/comment\\//.test(location.href) == false && $("#contents").length > 0) { if (window.addEventListener) { window.addEventListener("load", function () { ntm.push({ event: "nilSend", ni: ntmOption }); }, false); window.addEventListener("onpagehide" in window ? "pagehide" : "beforeunload", function () { ntm.push({ event: "nilSend", ni: ntmLeaveOption }); window.impression.sendImpressionNlog([], "disable"); }, false); } else if (window.attachEvent) { window.attachEvent("onload", function () { ntm.push({ event: "nilSend", ni: ntmOption }); }); window.attachEvent("onpagehide" in window ? "onpagehide" : "onbeforeunload", function () { ntm.push({ event: "nilSend", ni: ntmLeaveOption }); window.impression.sendImpressionNlog([], "disable"); }); } else { var onUnloadEventName = "onpagehide" in window ? "onpagehide" : "onbeforeunload"; var fnOnUnload = window[onUnloadEventName]; window[onUnloadEventName] = function () { ntm.push({ event: "nilSend", ni: ntmLeaveOption }); window.impression.sendImpressionNlog([], "disable"); if (fnOnUnload) { fnOnUnload(); } };

var fnOnload = window.onload; window.onload = function () { ntm.push({ event: "nilSend", ni: ntmOption }); if (fnOnload) { fnOnload(); } }; } } }

var eventType = "onpageshow" in window ? "pageshow" : "load"; $(window).on(eventType, function() { window.ntm = window.ntm || [];

var pageviewInfo = { event: "nLogPageViewService", nlogEvt: { page_layout: window.layoutName, news: window.getNLogNewsData() } };

pageviewInfo.nLogPageviewSti = "news"; pageviewInfo.nLogPageviewGdid = "880002EC_000000000000000000098199"; pageviewInfo.nlogEvt.news.sid1 = "101"; pageviewInfo.nlogEvt.news.oid = "243"; pageviewInfo.nlogEvt.news.aid = "0000098199";

window.ntm.push(pageviewInfo);

// ma 로그 전송 var data = { "service": "news", "baseInfo": { "category": (/\\/article\\/comment\\//.test(location.href) == true) ? "댓글" : "본문" }, "extraInfo": { "articleId": article.articleId, "sectionId": article.sectionId, "officeId": article.officeId, "articleType": article.type } };

if (typeof ma !== "undefined") { ma.init({ api: "https://ma.news.naver.com/l", data: data, contentBodyEl: document.querySelector("._article_content"), contentBodyVideoWrapperSelector: "._VIDEO_AREA" }); }

if (window.matchMedia("(prefers-color-scheme)").media === "not all") { $("html").removeClass("DARK_THEME"); }

setTimeout(function() { $("._OPACITY_FADE_TO").fadeTo(50, 1); }, 3000); });

", + "htmlBody": "

var svt = "20260524165614.743"; var timestamp = svt.substr(0, 8); var isLogin = false; var uhv = "";

var service = { news: false, entertain: false, sports: false, mnews: true, newsType: true }; var serviceName = "mnews";

var nilNtmUrl = "https://ntm.pstatic.net/scripts/ntm_b7032129a433.js";

var envPhase = "production"; var isProduction = true;

var gnb_service = "news"; var gnb_logout = encodeURIComponent(location.href); var gnb_template = "gnb_utf8"; var gnb_brightness = 1; var gnb_item_hide_option = 128;

var isDegradeMode = false; var isDegradeN2Mode = false;

\"이코노미스트\"

이코노미스트 언론사 구독되었습니다.\n메인 뉴스판에서 주요뉴스를\n볼 수 있습니다.\n보러가기\n닫기

이코노미스트 언론사 구독 해지되었습니다.\n닫기

[속보] "美와 합의시 이란 통제하에 호르무즈 선박수 회복"

입력 2026.05.24. 오후 4:54

텍스트 음성 변환 서비스 사용하기

이동 통신망을 이용하여 음성을 재생하면 별도의 데이터 통화료가 부과될 수 있습니다.

\"[속보]

[속보] "美와 합의시 이란 통제하에 호르무즈 선박수 회복"

[속보] "美와 합의시 이란 통제하에 호르무즈 선박수 회복"

김도정(dojeong@edaily.co.kr)

Copyright ⓒ 이코노미스트. All rights reserved. 무단 전재 및 재배포 금지.

이 기사는 언론사에서 경제 섹션으로 분류했습니다.

기사의 섹션 정보는 해당 언론사의 분류를 따르고 있습니다. 언론사는 개별 기사를 2개 이상 섹션으로 중복 분류할 수 있습니다.

이코노미스트 구독하고 메인에서 바로 만나보세요! 구독하고 메인에서 만나보세요!

언론사의 주요 뉴스를 메인에서 바로 만나보세요! 주요 뉴스를 메인에서 만나보세요!

이코노미스트\n\n주요뉴스 해당 언론사에서 선정하며 언론사(아웃링크) 로 이동합니다.

이코노미스트 언론사가 직접 선정한 이슈

window.__htExternalUrl = window.__htExternalUrl || {}; window.__htExternalUrl["like"] = "https://static-like.pstatic.net/js/reaction/dist/reaction.min.js"; window.__htLikeOption = { type: "multi", cssId: "news", domain: "https://news.like.naver.com", staticDomain: "https://static-like.pstatic.net", isMobile: true, isHiddenLabel: false, isHiddenCount: false, isHiddenZeroCount: false, isUsedLabelAsZeroCount: false, isDebugMode: false, isDuplication: false, isHiddenLayerAfterSelection: true, clicklog: function(el, name) { nclkWrap.send({ $this: $(el), sArea: name }); } }; window.__htLikeOption["isZeroFace"] = true;

기사 추천은 24시간 내 50회까지 참여할 수 있습니다.

window.__htExternalUrl = window.__htExternalUrl || {}; window.__htExternalUrl["share"] = "https://ssl.pstatic.net/spi/js/release/ko_KR/splugin.js"; window.__htShareOption = { "scriptUrl": window.__htExternalUrl["share"], "evKey": "news", "dimmed": "fixed", "serviceName": "뉴스", "sourceName": "" }; if (isAvailableDarkMode) { window.__htShareOption["darkMode"] = true; }

window.__htExternalUrl = window.__htExternalUrl || {}; window.__htExternalUrl["tip"] = "https://ssl.pstatic.net/dicimg/tip/tip_m.js"; window.__htExternalUrl["pcTip"] = "https://ssl.pstatic.net/dicimg/tip/tip_pc.js";

window.__htExternalUrl["handOff"] = { "category": "news_article", "apiDomain": "https://sidekick.fever.naver.com" };

\"섬네일

스티커 하나만 모아도 네이버페이 행운이!

선거 특집페이지에서 출석 스티커 받고, 최대 15,000P의 행운을 기대해요!

window.__htExternalUrl = window.__htExternalUrl || {}; window.__htExternalUrl["comment"] = "https://ssl.pstatic.net/static.cbox/js/cbox.core.js"; window.__htCboxOption = {"htMessage":{"template":{"user":{"profile":{"help":{"last":{"help_desc":"최근 30일간 작성한 댓글의 정보를 요약해 보여줍니다. 작성대비 본인의 삭제비율 (본인삭제율) 과 작성글이 받은 피드백 중 공감비율 (받은공감률) 을 표기합니다. 이때, 받은공감률은 댓글 삭제로 변동된 정보를 반영하지 않습니다."}},"title":"뉴스 댓글모음","private_back":"기사로 돌아가기"},"header":{"help":{"content":"댓글목록을 공개한 사용자의 댓글모음이 제공됩니다. 나의 댓글 모음에서 다른 사람에게 공개 또는 비공개 설정을 할 수 있습니다. 제공되는 목록에 삭제댓글/답글은 포함하지 않습니다.","title":"뉴스댓글 도움말"}}},"social_login_message":"계정을 선택하시면 로그인&middot;계정인증 을 통해 댓글을 남기실 수 있습니다. 뉴스서비스에서는 소셜 계정 사용이 불가하며 댓글모음만 확인하실 수 있습니다.","write_placeholder":"댓글을 입력해주세요","sort_favorite":"순공감순","duplicate_caution_link_address_pc":"https://news.naver.com/main/principle.naver","duplicate_caution_link_address_mobile":"https://m.news.naver.com/ombudsman/index?mode=rule&open=comment","duplicate_caution_link_title":"운영규정 자세히 알아보기","duplicate_caution_cont":"반복 등록이 계속될 경우 운영규정에 따라 서비스 이용이 정지될 수 있습니다.","duplicate_caution_title":"동일 내용 댓글의 반복 등록이 확인되었습니다.","write_long_placeholder":"다양한 의견이 서로 존중될 수 있도록 다른 사람에게 불쾌감을 주는 욕설, 혐오, 비하의 표현이나 타인의 권리를 침해하는 내용은 주의해주세요. 모든 작성자는 본인이 작성한 의견에 대해 법적 책임을 갖는다는 점 유의하시기 바랍니다.","help_titles":["댓글이력공개안내","댓글노출정책"],"info_comment_off":" 이코노미스트 댓글 정책에 따라 이 기사에서는 댓글을 제공하지 않습니다.","userblock_completed_dialog_content":"댓글모음이나 기사 댓글에서 차단을 해제할 수 있습니다.","userblock_ready_dialog_content":"이제 기사 댓글이나 댓글모음에서 상대방의 댓글을 확인할 수 없습니다.","userblocked_message_content":"댓글모음이나 기사 댓글에서 차단한 이용자들을 관리할 수 있습니다.","userunblock_completed_dialog_content":"상대방의 댓글모음이나 기사 댓글에서 언제든지 차단할 수 있습니다.","userunblock_ready_dialog_content":"이제 기사 댓글이나 댓글모음에서 상대방의 댓글을 확인할 수 있습니다.","view_all":"댓글 더보기","help_contents":["작성자가 삭제한 댓글(작성자 삭제), 운영자가 삭제한 댓글(규정 미준수) 그리고 삭제되지 않은 현재 남은 댓글의 수와 이력을 투명하게 제공합니다.","순공감순은 공감수에서 비공감수를 뺀 수치가 많은 댓글입니다. 비정상적인 방법으로 공감수가 증가하는 경우 제외될 수 있습니다."],"reply_close":"답글 접기","list_empty":"등록된 댓글이 없습니다.","comment_link":"이 기사의 댓글 실.

35개 언론사 52,918개 기사

12개 언론사 10,851개 기사

33개 언론사 25,820개 기사

27개 언론사 45,337개 기사

24개 언론사 31,751개 기사

18개 언론사 16,227개 기사

13개 언론사 27,114개 기사

이 기사를 본 이용자들이 함께 많이 본 기사, 해당 기사와 유사한 기사, 관심 기사 등을 자동 추천합니다

오후 1시~4시까지 집계한 결과입니다.

로그인\n\n전체서비스\n\n서비스안내\n\n오류신고\n\n고객센터\n\n기사배열 책임자 : 김수향\n청소년 보호 책임자 : 이정규\n\n각 언론사가 직접 콘텐츠를 편집합니다.\nⓒ 이코노미스트\n\n이 콘텐츠의 저작권은 저작권자 또는 제공처에 있으며, 이를 무단\n이용하는 경우 저작권법 등에 따라 법적 책임을 질 수 있습니다.\n\nNAVER

{{#series}} {{value}} 연재 구독을 취소하시겠습니까? {{/series}} {{#journalist}} {{value}} 기자의 기사 구독을 취소하시겠습니까? {{/journalist}}

{{officeName}} 구독이 완료되었습니다. 알림 으로 받아 보시겠습니까?

{{officeName}} 구독이 완료되었습니다. 알림 으로 받아 보시겠습니까?

네이버 앱 푸시알림이 꺼져있어요.

네이버 앱 알림켜기 > 알림탭 > 알림허용

버튼을 눌러 앱 알림 설정을 켜주세요.

{{#content.image}}

{{#content.image.class}}

{{/content.image.class}}

{{/content.image}} {{#isTextType}}

{{#content.image}}

{{#content.image.class}}

{{/content.image.class}}

{{/content.image}} {{#isTextType}} {{content.title}}

{{/isTextType}} {{#isTitleType}}

{{content.headTitle}} {{content.title}}

{{content.description}}

{{content.cancelMessage}}

{{content.confirmMessage}}

{{toastTitle}} {{toastMessage}}

{{officeName}} 언론사 알림이 설정되었습니다.{{#isSupportedNaverappNotificationSetting}} 네이버앱 전체 푸시 알림도 활성화된 상태입니다.{{/isSupportedNaverappNotificationSetting}}

window.ntm = window.ntm || []; var ntmOption = {};

var section = {"id":"101","name":"경제"}; var redirectSection = {"id":"101","name":"경제"};

ntmOption["channelId"] = article.officeId;

// 기타 또는 주요 섹션 이름 var nilSendSectionName = "기타"; var nilSendSectionId = ""; if (/100|101|102|103|104|105|106|107/.test(section.id)) { nilSendSectionName = section.name; nilSendSectionId = section.id; }

// 기사 타입 var htArticleTypeName = { "0": "text", "1": "photo", "2": "tv" }; var articleTypeName = htArticleTypeName[article.type] || "";

ntmOption["hitType"] = "cv"; ntmOption["eventCategory"] = "post_view"; ntmOption["uri"] = "http://news.naver.com/main/read.nhn?oid=243&aid=0000098199"; ntmOption["gdid"] = article.gdid; ntmOption["dimension1"] = encodeURIComponent(nilSendSectionName); ntmOption["dimension4"] = articleTypeName; ntmOption["optional2"] = nilSendSectionId;

var journalistIdList = [];

// 기자 목록 $("._JOURNALIST_CARD ._JOURNALIST_ID").each(function (index, object) { var $this = $(object); var journalistId = $this.attr("data-journalistId");

if (journalistId && journalistIdList.indexOf(journalistId) === -1) { journalistIdList.push(journalistId); } });

if (journalistIdList.length > 0) { ntmOption["dimension3"] = journalistIdList.join("___"); }

if (location.href.match(/ntype=RANKING/)) { ntmOption["dimension5"] = "y"; }

if (service.sports || service.newsType) { ntmOption["dimension2"] = article.sectionInfo.secondSection + "." + article.sectionInfo.thirdSection; }

var seriesComponentId = $("._VALID_SERIES").attr("data-component-id"); ntmOption["dimension6"] = seriesComponentId;

var issueComponentId = $("._ISSUE_ARTICLE_INFO").attr("data-component-id"); ntmOption["dimension8"] = issueComponentId;

if (/[?|&]type=breakingnews(&|$)/.test(location.href) === true) { if (/(m\\.naver\\.com)|(media\\.naver\\.com)|(news\\.naver\\.com\\/?$)|(www\\.naver\\.com)/.test(document.referrer) === true) { ntmOption["dimension7"] = "breaking"; } }

var ntmLeaveOption = $.extend({}, ntmOption); ntmLeaveOption["hitType"] = "event"; ntmLeaveOption["eventCategory"] = "action"; ntmLeaveOption["eventAction"] = "leave";

if (/\\/article\\/comment\\//.test(location.href) == false && $("#contents").length > 0) { if (window.addEventListener) { window.addEventListener("load", function () { ntm.push({ event: "nilSend", ni: ntmOption }); }, false); window.addEventListener("onpagehide" in window ? "pagehide" : "beforeunload", function () { ntm.push({ event: "nilSend", ni: ntmLeaveOption }); window.impression.sendImpressionNlog([], "disable"); }, false); } else if (window.attachEvent) { window.attachEvent("onload", function () { ntm.push({ event: "nilSend", ni: ntmOption }); }); window.attachEvent("onpagehide" in window ? "onpagehide" : "onbeforeunload", function () { ntm.push({ event: "nilSend", ni: ntmLeaveOption }); window.impression.sendImpressionNlog([], "disable"); }); } else { var onUnloadEventName = "onpagehide" in window ? "onpagehide" : "onbeforeunload"; var fnOnUnload = window[onUnloadEventName]; window[onUnloadEventName] = function () { ntm.push({ event: "nilSend", ni: ntmLeaveOption }); window.impression.sendImpressionNlog([], "disable"); if (fnOnUnload) { fnOnUnload(); } };

var fnOnload = window.onload; window.onload = function () { ntm.push({ event: "nilSend", ni: ntmOption }); if (fnOnload) { fnOnload(); } }; } } }

var eventType = "onpageshow" in window ? "pageshow" : "load"; $(window).on(eventType, function() { window.ntm = window.ntm || [];

var pageviewInfo = { event: "nLogPageViewService", nlogEvt: { page_layout: window.layoutName, news: window.getNLogNewsData() } };

pageviewInfo.nLogPageviewSti = "news"; pageviewInfo.nLogPageviewGdid = "880002EC_000000000000000000098199"; pageviewInfo.nlogEvt.news.sid1 = "101"; pageviewInfo.nlogEvt.news.oid = "243"; pageviewInfo.nlogEvt.news.aid = "0000098199";

window.ntm.push(pageviewInfo);

// ma 로그 전송 var data = { "service": "news", "baseInfo": { "category": (/\\/article\\/comment\\//.test(location.href) == true) ? "댓글" : "본문" }, "extraInfo": { "articleId": article.articleId, "sectionId": article.sectionId, "officeId": article.officeId, "articleType": article.type } };

if (typeof ma !== "undefined") { ma.init({ api: "https://ma.news.naver.com/l", data: data, contentBodyEl: document.querySelector("._article_content"), contentBodyVideoWrapperSelector: "._VIDEO_AREA" }); }

if (window.matchMedia("(prefers-color-scheme)").media === "not all") { $("html").removeClass("DARK_THEME"); }

setTimeout(function() { $("._OPACITY_FADE_TO").fadeTo(50, 1); }, 3000); });

", "tags": [ "네이버뉴스", "경제" @@ -3669,24 +4149,3 @@ } ] } -title": "선거운동원에게 인사 건네는 하정우 [포토]", - "sourceLabel": "더팩트", - "url": "https://n.news.naver.com/mnews/article/629/0000501066", - "lead": "[더팩트 | 부산=박상민 기자] 하정우 더불어민주당 부산 북구갑 국회의원 후보가 21일 오전 부산 북구 구포대교 사거리에서 출정식을 연 가운데 한 선거운동원에게 인사를 건네고 있다", - "body": "[더팩트 | 부산=박상민 기자] 하정우 더불어민주당 부산 북구갑 국회의원 후보가 21일 오전 부산 북구 구포대교 사거리에서 출정식을 연 가운데 한 선거운동원에게 인사를 건네고 있다\n\n발로 뛰는 더팩트는 24시간 여러분의 제보를 기다립니다. ▶카카오톡: '더팩트제보' 검색 ▶이메일: jebo@tf.co.kr ▶뉴스 홈페이지: http://talk.tf.co.kr/bbs/report/write", - "htmlBody": "
\"기사

[더팩트 | 부산=박상민 기자] 하정우 더불어민주당 부산 북구갑 국회의원 후보가 21일 오전 부산 북구 구포대교 사거리에서 출정식을 연 가운데 한 선거운동원에게 인사를 건네고 있다

발로 뛰는 더팩트는 24시간 여러분의 제보를 기다립니다. ▶카카오톡: '더팩트제보' 검색 ▶이메일: jebo@tf.co.kr ▶뉴스 홈페이지: http://talk.tf.co.kr/bbs/report/write

", - "tags": [ - "네이버뉴스", - "정치" - ], - "signals": [ - "상단노출" - ], - "listedDate": "2026-05-21", - "publishedAt": "2026-05-21T08:30:10+09:00" - }, - "createdAt": "2026-05-20T23:30:53.755Z", - "updatedAt": "2026-05-20T23:30:53.755Z" - } - ] -} diff --git a/etc/servers/work-server/docker-compose.yml b/etc/servers/work-server/docker-compose.yml index 0271b60..c3d7d65 100644 --- a/etc/servers/work-server/docker-compose.yml +++ b/etc/servers/work-server/docker-compose.yml @@ -1,9 +1,25 @@ services: work-server: + image: nginx:1.27-alpine + container_name: work-server + logging: + driver: json-file + options: + max-size: "200m" + max-file: "2" + mem_limit: 256m + ports: + - '127.0.0.1:3100:3100' + volumes: + - ./.docker/proxy/default.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - work-backend + + work-server-blue: build: context: . dockerfile: Dockerfile - container_name: work-server + container_name: work-server-blue logging: driver: json-file options: @@ -19,8 +35,6 @@ services: required: false - path: ./.env required: false - ports: - - '127.0.0.1:3100:3100' volumes: - ./:/app - work-server-node-modules:/app/node_modules @@ -43,6 +57,57 @@ services: DB_TIME_ZONE: ${DB_TIME_ZONE:-Asia/Seoul} NPM_CONFIG_CACHE: /home/how2ice/.npm WORK_SERVER_DIST_DIR: /app/dist + WORK_SERVER_SLOT: blue + SERVER_COMMAND_DOCKER_SOCKET: ${SERVER_COMMAND_DOCKER_SOCKET:-/var/run/docker.sock} + DOCKER_HOST: ${DOCKER_HOST:-} + networks: + - work-backend + extra_hosts: + - 'host.docker.internal:host-gateway' + + work-server-green: + build: + context: . + dockerfile: Dockerfile + container_name: work-server-green + logging: + driver: json-file + options: + max-size: "200m" + max-file: "2" + user: "0:0" + group_add: + - "${SERVER_COMMAND_DOCKER_GID:-984}" + mem_limit: 2048m + working_dir: /app + env_file: + - path: ./.env.example + required: false + - path: ./.env + required: false + volumes: + - ./:/app + - work-server-node-modules:/app/node_modules + - ../../../:/workspace/main-project + - ../../../.auto_codex:/workspace/auto_codex + - ../../../scripts:/workspace/repo-scripts:ro + - ${SERVER_COMMAND_DOCKER_SOCKET:-/var/run/docker.sock}:${SERVER_COMMAND_DOCKER_SOCKET:-/var/run/docker.sock} + - ./.docker/home:/home/how2ice + - ./.docker/codex-home:/codex-home + - ./.docker/codex-home-template:/codex-home-template + environment: + TZ: ${APP_TIME_ZONE:-Asia/Seoul} + HOME: /home/how2ice + CODEX_HOME: /codex-home + PLAN_CODEX_TEMPLATE_HOME: /codex-home-template + PLAN_CODEX_BIN: ${PLAN_CODEX_BIN:-codex} + PLAN_CODEX_ENABLED: ${PLAN_CODEX_ENABLED:-false} + PLAN_WORKER_ENABLED: ${PLAN_WORKER_ENABLED:-false} + APP_TIME_ZONE: ${APP_TIME_ZONE:-Asia/Seoul} + DB_TIME_ZONE: ${DB_TIME_ZONE:-Asia/Seoul} + NPM_CONFIG_CACHE: /home/how2ice/.npm + WORK_SERVER_DIST_DIR: /app/dist + WORK_SERVER_SLOT: green SERVER_COMMAND_DOCKER_SOCKET: ${SERVER_COMMAND_DOCKER_SOCKET:-/var/run/docker.sock} DOCKER_HOST: ${DOCKER_HOST:-} networks: diff --git a/etc/servers/work-server/src/app.ts b/etc/servers/work-server/src/app.ts index ab33680..27411b1 100644 --- a/etc/servers/work-server/src/app.ts +++ b/etc/servers/work-server/src/app.ts @@ -7,12 +7,14 @@ import { registerDdlRoutes } from './routes/ddl.js'; import { registerErrorLogRoutes } from './routes/error-log.js'; import { registerHealthRoutes } from './routes/health.js'; import { registerAppConfigRoutes } from './routes/app-config.js'; +import { registerBaseballTicketBayRoutes } from './routes/baseball-ticket-bay.js'; import { registerChatRoutes } from './routes/chat.js'; import { registerNotificationRoutes } from './routes/notification.js'; import { registerPlanRoutes } from './routes/plan.js'; import { registerPhotoPrismRoutes } from './routes/photoprism.js'; import { registerReaderRoutes } from './routes/reader.js'; import { registerResourceManagerRoutes } from './routes/resource-manager.js'; +import { registerRuntimeRoutes } from './routes/runtime.js'; import { registerServerCommandRoutes } from './routes/server-command.js'; import { registerSchemaRoutes } from './routes/schema.js'; import { registerSharedResourceTokenRoutes } from './routes/shared-resource-token.js'; @@ -22,6 +24,20 @@ import { registerTextMemoRoutes } from './routes/text-memo.js'; import { registerVisitorHistoryRoutes } from './routes/visitor-history.js'; import { shouldPersistNotFoundErrorLog } from './not-found.js'; import { createErrorLog } from './services/error-log-service.js'; +import { + isRuntimeDraining, + trackHttpRequestFinished, + trackHttpRequestStarted, +} from './services/runtime-drain-service.js'; + +function isDrainAllowedPath(method: string, url: string) { + return method === 'OPTIONS' + || url === '/' + || url === '/api' + || url === '/health' + || url.startsWith('/api/runtime') + || url.startsWith('/api/server-commands'); +} export function createApp() { const app = Fastify({ @@ -35,10 +51,37 @@ export function createApp() { origin: true, }); + app.addHook('onRequest', async (request, reply) => { + trackHttpRequestStarted(); + let finished = false; + const finalize = () => { + if (finished) { + return; + } + + finished = true; + trackHttpRequestFinished(); + reply.raw.off('finish', finalize); + reply.raw.off('close', finalize); + }; + + reply.raw.on('finish', finalize); + reply.raw.on('close', finalize); + + if (isRuntimeDraining() && !isDrainAllowedPath(request.method, request.url)) { + reply.code(503).send({ + ok: false, + message: '이 서버는 배포 전환 중이라 새 요청을 받지 않습니다. 잠시 후 다시 시도해 주세요.', + }); + return reply; + } + }); + registerJsonBodyParser(app); app.register(registerBoardRoutes); app.register(registerHealthRoutes); app.register(registerAppConfigRoutes); + app.register(registerBaseballTicketBayRoutes); app.register(registerChatRoutes); app.register(registerSchemaRoutes); app.register(registerDdlRoutes); @@ -51,6 +94,7 @@ export function createApp() { app.register(registerPhotoPrismRoutes); app.register(registerReaderRoutes); app.register(registerResourceManagerRoutes); + app.register(registerRuntimeRoutes); app.register(registerSharedResourceTokenRoutes); app.register(registerServerCommandRoutes); app.register(registerTextMemoRoutes); diff --git a/etc/servers/work-server/src/config/env.ts b/etc/servers/work-server/src/config/env.ts index 3eeea0e..7c7e15b 100644 --- a/etc/servers/work-server/src/config/env.ts +++ b/etc/servers/work-server/src/config/env.ts @@ -79,6 +79,7 @@ const envSchema = z.object({ SERVER_COMMAND_RUNNER_URL: z.string().default('http://host.docker.internal:3211/health'), SERVER_COMMAND_RUNNER_ACCESS_TOKEN: z.string().default('local-server-command-runner'), SERVER_COMMAND_RUNNER_HEARTBEAT_FILE: z.string().optional(), + SERVER_COMMAND_WORK_SERVER_ACTIVE_SLOT_FILE: z.string().optional(), SERVER_COMMAND_TEST_SERVICE: z.string().default('app'), SERVER_COMMAND_REL_SERVICE: z.string().default('release-app'), SERVER_COMMAND_PROD_SERVICE: z.string().default('prod-app'), diff --git a/etc/servers/work-server/src/routes/baseball-ticket-bay.ts b/etc/servers/work-server/src/routes/baseball-ticket-bay.ts new file mode 100644 index 0000000..2c0d8b2 --- /dev/null +++ b/etc/servers/work-server/src/routes/baseball-ticket-bay.ts @@ -0,0 +1,205 @@ +import type { FastifyInstance } from 'fastify'; +import { z } from 'zod'; +import { + createBaseballTicketBayAlert, + createBaseballTicketBayLog, + deleteBaseballTicketBayLog, + deleteBaseballTicketBayAlert, + listBaseballTicketBayAlerts, + listBaseballTicketBayLogs, + runBaseballTicketBayAlert, + searchBaseballTicketBayListings, + updateBaseballTicketBayAlert, +} from '../services/baseball-ticket-bay-service.js'; + +const timeWindowSchema = z.object({ + id: z.string().trim().min(1), + start: z.string().trim().regex(/^\d{2}:\d{2}$/), + end: z.string().trim().regex(/^\d{2}:\d{2}$/), +}); + +const alertPayloadSchema = z.object({ + title: z.string().trim().min(1).max(255), + eventDate: z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/), + team: z.string().trim().min(1).max(50), + zone: z.string().trim().min(1).max(100), + aisleSide: z.string().trim().min(1).max(100), + seatDirections: z.array(z.string().trim().min(1).max(50)).max(10), + maxPrice: z.number().finite().positive().nullable(), + seatCount: z.number().int().positive().max(10), + batchIntervalMinutes: z.number().int().min(1).max(120), + sameProductAlertEnabled: z.boolean(), + sameProductNotifyOnce: z.boolean(), + active: z.boolean().default(true), + timeWindows: z.array(timeWindowSchema).min(1).max(24), +}); + +function readHeader(request: { headers: Record }, key: string) { + const raw = request.headers[key]; + return Array.isArray(raw) ? String(raw[0] ?? '').trim() : String(raw ?? '').trim(); +} + +export async function registerBaseballTicketBayRoutes(app: FastifyInstance) { + app.post('/api/baseball-ticket-bay/search', async (request) => searchBaseballTicketBayListings(request.body ?? {})); + + app.get('/api/baseball-ticket-bay/alerts', async (request, reply) => { + const clientId = readHeader(request, 'x-client-id'); + + if (!clientId) { + return reply.code(400).send({ message: '클라이언트 ID가 없어 알림 목록을 불러올 수 없습니다.' }); + } + + return { + ok: true, + items: await listBaseballTicketBayAlerts(clientId), + }; + }); + + app.get('/api/baseball-ticket-bay/logs', async (request, reply) => { + const clientId = readHeader(request, 'x-client-id'); + + if (!clientId) { + return reply.code(400).send({ message: '클라이언트 ID가 없어 로그를 불러올 수 없습니다.' }); + } + + const query = z.object({ alertId: z.string().trim().min(1).optional() }).parse(request.query ?? {}); + + return { + ok: true, + items: await listBaseballTicketBayLogs(clientId, query.alertId), + }; + }); + + app.delete('/api/baseball-ticket-bay/logs/:id', async (request, reply) => { + const clientId = readHeader(request, 'x-client-id'); + + if (!clientId) { + return reply.code(400).send({ message: '클라이언트 ID가 없어 로그를 삭제할 수 없습니다.' }); + } + + const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {}); + const item = await deleteBaseballTicketBayLog(params.id, clientId); + + if (!item) { + return reply.code(404).send({ message: '삭제할 로그를 찾지 못했습니다.' }); + } + + return { + ok: true, + item, + }; + }); + + app.post('/api/baseball-ticket-bay/alerts', async (request, reply) => { + const clientId = readHeader(request, 'x-client-id'); + + if (!clientId) { + return reply.code(400).send({ message: '클라이언트 ID가 없어 알림을 저장할 수 없습니다.' }); + } + + const payload = alertPayloadSchema.parse(request.body ?? {}); + const item = await createBaseballTicketBayAlert(payload, { + clientId, + appOrigin: readHeader(request, 'x-app-origin'), + appDomain: readHeader(request, 'x-app-domain'), + }); + await createBaseballTicketBayLog({ + clientId, + alertId: item.id, + alertTitle: item.title, + action: 'create', + status: 'info', + message: '알림 조건을 저장했습니다.', + detail: `${item.team} · ${item.eventDate}`, + }); + + return { + ok: true, + item, + }; + }); + + app.patch('/api/baseball-ticket-bay/alerts/:id', async (request, reply) => { + const clientId = readHeader(request, 'x-client-id'); + + if (!clientId) { + return reply.code(400).send({ message: '클라이언트 ID가 없어 알림을 수정할 수 없습니다.' }); + } + + const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {}); + const payload = alertPayloadSchema.partial().parse(request.body ?? {}); + const item = await updateBaseballTicketBayAlert(params.id, payload, { + clientId, + appOrigin: readHeader(request, 'x-app-origin'), + appDomain: readHeader(request, 'x-app-domain'), + }); + await createBaseballTicketBayLog({ + clientId, + alertId: item.id, + alertTitle: item.title, + action: payload.active === false ? 'pause' : payload.active === true ? 'resume' : 'run', + status: 'info', + message: + payload.active === false + ? '알림을 중지했습니다.' + : payload.active === true + ? '알림을 다시 실행 상태로 전환했습니다.' + : '알림 조건을 수정 저장했습니다.', + detail: `${item.team} · ${item.eventDate}`, + }); + + return { + ok: true, + item, + }; + }); + + app.delete('/api/baseball-ticket-bay/alerts/:id', async (request, reply) => { + const clientId = readHeader(request, 'x-client-id'); + + if (!clientId) { + return reply.code(400).send({ message: '클라이언트 ID가 없어 알림을 삭제할 수 없습니다.' }); + } + + const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {}); + const item = await deleteBaseballTicketBayAlert(params.id, clientId); + + if (!item) { + return reply.code(404).send({ message: '삭제할 알림을 찾지 못했습니다.' }); + } + + await createBaseballTicketBayLog({ + clientId, + alertId: item.id, + alertTitle: item.title, + action: 'delete', + status: 'info', + message: '알림 항목을 삭제했습니다.', + detail: `${item.team} · ${item.eventDate}`, + }); + + return { + ok: true, + item, + }; + }); + + app.post('/api/baseball-ticket-bay/alerts/:id/run', async (request, reply) => { + const clientId = readHeader(request, 'x-client-id'); + + if (!clientId) { + return reply.code(400).send({ message: '클라이언트 ID가 없어 즉시 실행할 수 없습니다.' }); + } + + const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {}); + const result = await runBaseballTicketBayAlert(params.id, { ignoreTimeWindow: true }); + + return { + ok: true, + alert: result.alert, + matches: result.matches, + notifiedMatches: result.notifiedMatches, + log: result.log, + }; + }); +} diff --git a/etc/servers/work-server/src/routes/chat.ts b/etc/servers/work-server/src/routes/chat.ts index 91a6a10..5a3f950 100644 --- a/etc/servers/work-server/src/routes/chat.ts +++ b/etc/servers/work-server/src/routes/chat.ts @@ -23,9 +23,11 @@ import { import { rollbackChatRuntimeRequest } from '../services/chat-runtime-rollback-service.js'; import { assignSharedResourceTokenToRequests, + appendChatConversationActivityLine, appendChatConversationMessage, buildChatPromptTargetSignature, CHAT_CONTEXT_DESCRIPTION_MAX_LENGTH, + cancelUnansweredChatConversationRequest, clearChatConversationData, createChatConversation, deleteUnansweredChatConversationRequest, @@ -90,6 +92,15 @@ const chatPromptContextRefSchema = z .optional() .nullable(); +const chatComposerAttachmentSchema = z.object({ + id: z.string().trim().min(1).max(240), + name: z.string().trim().min(1).max(500), + path: z.string().trim().min(1).max(4000), + publicUrl: z.string().trim().min(1).max(4000), + size: z.number().finite().min(0).max(CHAT_ATTACHMENT_FILE_SIZE_LIMIT), + mimeType: z.string().trim().min(1).max(255), +}); + async function findExistingActivePromptFollowupRequest( sessionId: string, parentRequestId: string, @@ -244,6 +255,127 @@ function createManagedChatShareMessageIds() { }; } +function sortShareMessages(messages: ListedChatConversationMessage[]) { + return [...messages].sort((left, right) => { + if (left.id !== right.id) { + return left.id - right.id; + } + + return left.timestamp.localeCompare(right.timestamp); + }); +} + +function sortShareRequests(requests: ListedChatConversationRequest[]) { + return [...requests].sort((left, right) => { + const createdAtDiff = left.createdAt.localeCompare(right.createdAt); + + if (createdAtDiff !== 0) { + return createdAtDiff; + } + + return left.requestId.localeCompare(right.requestId); + }); +} + +async function materializeManagedShareConversation(args: { + shareSnapshot: NonNullable>>; + managedResourceTokenId: string; + ownerClientId: string | null; + shareTitle: string; +}) { + const { shareSnapshot, managedResourceTokenId, ownerClientId, shareTitle } = args; + const sessionId = createManagedChatShareSessionId(); + const requestIdSet = new Set(shareSnapshot.requests.map((item) => item.requestId.trim()).filter(Boolean)); + const sortedRequests = sortShareRequests(shareSnapshot.requests); + const sortedMessages = sortShareMessages(shareSnapshot.messages); + const sourceConversation = shareSnapshot.conversation; + + await createChatConversation({ + sessionId, + clientId: ownerClientId, + title: shareTitle, + draftText: '', + requestBadgeLabel: sourceConversation?.requestBadgeLabel ?? null, + codexModel: sourceConversation?.codexModel ?? null, + chatTypeId: sourceConversation?.chatTypeId ?? sourceConversation?.lastChatTypeId ?? null, + lastChatTypeId: sourceConversation?.lastChatTypeId ?? sourceConversation?.chatTypeId ?? null, + generalSectionName: sourceConversation?.generalSectionName ?? null, + contextLabel: sourceConversation?.contextLabel ?? null, + contextDescription: sourceConversation?.contextDescription ?? null, + notifyOffline: true, + }); + + for (const message of sortedMessages) { + await appendChatConversationMessage( + { + sessionId, + clientId: ownerClientId, + title: shareTitle, + requestBadgeLabel: sourceConversation?.requestBadgeLabel ?? null, + codexModel: sourceConversation?.codexModel ?? null, + chatTypeId: sourceConversation?.chatTypeId ?? sourceConversation?.lastChatTypeId ?? null, + lastChatTypeId: sourceConversation?.lastChatTypeId ?? sourceConversation?.chatTypeId ?? null, + generalSectionName: sourceConversation?.generalSectionName ?? null, + contextLabel: sourceConversation?.contextLabel ?? null, + contextDescription: sourceConversation?.contextDescription ?? null, + notifyOffline: true, + }, + { + sessionId, + messageId: message.id, + author: message.author, + text: message.text, + timestamp: message.timestamp, + clientRequestId: message.clientRequestId?.trim() || null, + parts: message.parts ?? [], + }, + ); + } + + for (const request of sortedRequests) { + const normalizedParentRequestId = request.parentRequestId?.trim() || ''; + + await upsertChatConversationRequest(sessionId, { + requestId: request.requestId, + requesterClientId: request.requesterClientId ?? ownerClientId, + chatTypeId: request.chatTypeId ?? sourceConversation?.chatTypeId ?? sourceConversation?.lastChatTypeId ?? null, + chatTypeLabel: request.chatTypeLabel ?? sourceConversation?.contextLabel ?? null, + requestOrigin: request.requestOrigin, + sharedResourceTokenId: managedResourceTokenId, + parentRequestId: normalizedParentRequestId && requestIdSet.has(normalizedParentRequestId) ? normalizedParentRequestId : null, + status: request.status, + statusMessage: request.statusMessage, + userMessageId: request.userMessageId, + userText: request.userText, + responseMessageId: request.responseMessageId, + responseText: request.responseText, + usageSnapshot: request.usageSnapshot, + totalTokens: request.totalTokens, + }); + } + + for (const activityLog of shareSnapshot.activityLogs) { + let lineNo = 1; + for (const line of activityLog.lines) { + await appendChatConversationActivityLine(sessionId, activityLog.requestId, line, lineNo); + lineNo += 1; + } + } + + await assignSharedResourceTokenToRequests( + sessionId, + sortedRequests.map((item) => item.requestId), + managedResourceTokenId, + ); + + return { + sessionId, + requestId: + sortedRequests.find((item) => item.requestId.trim() === shareSnapshot.targetRequest.requestId.trim())?.requestId + ?? shareSnapshot.targetRequest.requestId, + }; +} + export function resolveStaticContentType(filePath: string) { const extension = path.extname(filePath).toLowerCase(); @@ -1546,6 +1678,30 @@ export async function registerChatRoutes(app: FastifyInstance) { }); } + const shareSnapshot = await buildChatShareSnapshot({ + version: CHAT_SHARE_TOKEN_VERSION, + kind: payload.kind, + sessionId: payload.sessionId, + requestId: payload.requestId, + tokenSettingId: tokenSetting.id, + tokenSettingName: tokenSetting.name, + tokenSettingDefaultExpiresInMinutes: tokenSetting.defaultExpiresInMinutes, + tokenSettingAllowedAppIds: tokenSetting.allowedAppIds, + tokenSettingMaxTokensPer30Days: tokenSetting.maxTokensPer30Days, + tokenSettingMaxTokensPer7Days: tokenSetting.maxTokensPer7Days, + tokenSettingMaxTokensPer5Hours: tokenSetting.maxTokensPer5Hours, + tokenSettingOneTimeTokenLimit: tokenSetting.oneTimeTokenLimit, + sourceMessageId: payload.sourceMessageId, + promptIndex: payload.promptIndex, + promptSignature: payload.promptSignature, + }); + + if (!shareSnapshot) { + return reply.code(404).send({ + message: '공유할 채팅 범위를 찾을 수 없습니다.', + }); + } + if (payload.kind === 'prompt') { if (payload.promptIndex == null || !payload.promptSignature) { return reply.code(400).send({ @@ -1553,25 +1709,7 @@ export async function registerChatRoutes(app: FastifyInstance) { }); } - const shareSnapshot = await buildChatShareSnapshot({ - version: CHAT_SHARE_TOKEN_VERSION, - kind: payload.kind, - sessionId: payload.sessionId, - requestId: payload.requestId, - tokenSettingId: tokenSetting.id, - tokenSettingName: tokenSetting.name, - tokenSettingDefaultExpiresInMinutes: tokenSetting.defaultExpiresInMinutes, - tokenSettingAllowedAppIds: tokenSetting.allowedAppIds, - tokenSettingMaxTokensPer30Days: tokenSetting.maxTokensPer30Days, - tokenSettingMaxTokensPer7Days: tokenSetting.maxTokensPer7Days, - tokenSettingMaxTokensPer5Hours: tokenSetting.maxTokensPer5Hours, - tokenSettingOneTimeTokenLimit: tokenSetting.oneTimeTokenLimit, - sourceMessageId: payload.sourceMessageId, - promptIndex: payload.promptIndex, - promptSignature: payload.promptSignature, - }); - - if (!shareSnapshot?.promptTarget) { + if (!shareSnapshot.promptTarget) { return reply.code(404).send({ message: '공유할 prompt 대상을 찾을 수 없습니다.', }); @@ -1581,6 +1719,12 @@ export async function registerChatRoutes(app: FastifyInstance) { const managedResourceTokenId = createManagedChatShareTokenId(); const token = randomUUID().replace(/-/g, '').slice(0, 24); const sharePath = resolveChatSharePath(token); + const managedShareConversation = await materializeManagedShareConversation({ + shareSnapshot, + managedResourceTokenId, + ownerClientId: clientId || null, + shareTitle: payload.name, + }); await upsertSharedResourceToken({ id: managedResourceTokenId, @@ -1599,8 +1743,8 @@ export async function registerChatRoutes(app: FastifyInstance) { }, resourceContext: { kind: payload.kind, - sessionId: payload.sessionId, - requestId: payload.requestId, + sessionId: managedShareConversation.sessionId, + requestId: managedShareConversation.requestId, sourceMessageId: payload.sourceMessageId ?? null, promptIndex: payload.promptIndex ?? null, promptSignature: payload.promptSignature ?? null, @@ -1620,33 +1764,6 @@ export async function registerChatRoutes(app: FastifyInstance) { usageLimit: 0, }); - const createdShareSnapshot = await buildChatShareSnapshot({ - version: CHAT_SHARE_TOKEN_VERSION, - kind: payload.kind, - sessionId: payload.sessionId, - requestId: payload.requestId, - tokenSettingId: tokenSetting.id, - tokenSettingName: tokenSetting.name, - tokenSettingDefaultExpiresInMinutes: tokenSetting.defaultExpiresInMinutes, - tokenSettingAllowedAppIds: tokenSetting.allowedAppIds, - tokenSettingMaxTokensPer30Days: tokenSetting.maxTokensPer30Days, - tokenSettingMaxTokensPer7Days: tokenSetting.maxTokensPer7Days, - tokenSettingMaxTokensPer5Hours: tokenSetting.maxTokensPer5Hours, - tokenSettingOneTimeTokenLimit: tokenSetting.oneTimeTokenLimit, - managedResourceTokenId, - sourceMessageId: payload.sourceMessageId, - promptIndex: payload.promptIndex, - promptSignature: payload.promptSignature, - }); - - if (createdShareSnapshot?.requests.length) { - await assignSharedResourceTokenToRequests( - payload.sessionId, - createdShareSnapshot.requests.map((item) => item.requestId), - managedResourceTokenId, - ); - } - return { ok: true, token, @@ -2060,6 +2177,7 @@ export async function registerChatRoutes(app: FastifyInstance) { }), ).max(20).optional(), summaryText: z.string().max(10000).optional().nullable(), + attachments: z.array(chatComposerAttachmentSchema).max(20).optional(), followupText: z.string().trim().min(1).max(20000), contextRef: chatPromptContextRefSchema, }).parse(request.body ?? {}); @@ -2265,6 +2383,237 @@ export async function registerChatRoutes(app: FastifyInstance) { }; }); + app.post(`${CHAT_SHARE_ROUTE_PREFIX}/:token/request-cancel`, async (request, reply) => { + const params = z.object({ + token: z.string().trim().min(1).max(16000), + }).parse(request.params ?? {}); + const payload = z.object({ + parentRequestId: z.string().trim().min(1).max(120), + }).parse(request.body ?? {}); + const managedContext = await resolveManagedChatShareContext(params.token); + const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token); + + if (!tokenPayload) { + return reply.code(404).send({ + message: '공유 링크가 유효하지 않습니다.', + }); + } + + const shareSnapshot = await buildChatShareSnapshot(tokenPayload); + + if (!shareSnapshot) { + return reply.code(404).send({ + message: '공유 대상을 찾을 수 없습니다.', + }); + } + + const unavailableMessage = resolveManagedShareUnavailableMessage(managedContext.managedResource); + + if (unavailableMessage) { + return reply.code(403).send({ + message: unavailableMessage, + }); + } + + if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) { + return; + } + + if (managedContext.managedResource?.token.permissions && !managedContext.managedResource.token.permissions.includes('comment')) { + return reply.code(403).send({ + message: '이 공유 링크에는 요청 취소 처리 권한이 없습니다.', + }); + } + + const normalizedParentRequestId = resolveRecoveredShareParentRequestId( + shareSnapshot, + payload.parentRequestId, + [shareSnapshot.targetRequest.requestId], + ); + + if (!normalizedParentRequestId || !shareSnapshot.requests.some((request) => request.requestId.trim() === normalizedParentRequestId)) { + return reply.code(403).send({ + message: '이 공유 링크 범위를 벗어난 요청입니다.', + }); + } + + if (tokenPayload.kind === 'prompt' && normalizedParentRequestId !== tokenPayload.requestId) { + return reply.code(403).send({ + message: '이 공유 링크에서 허용되지 않은 요청입니다.', + }); + } + + const targetRequest = shareSnapshot.requests.find((request) => request.requestId.trim() === normalizedParentRequestId) ?? null; + + if ( + !targetRequest + || targetRequest.hasResponse + || targetRequest.status !== 'failed' + || (targetRequest.statusMessage?.trim() ?? '') !== '중단된 오래된 요청' + ) { + return reply.code(409).send({ + message: '지금은 이 요청을 취소 처리할 수 없습니다.', + }); + } + + const result = await cancelUnansweredChatConversationRequest( + tokenPayload.sessionId, + normalizedParentRequestId, + '사용자 요청으로 중단된 요청을 취소 처리했습니다.', + ); + + if (!result.cancelled || !result.item) { + if (result.reason === 'answered') { + return reply.code(409).send({ + message: '이미 답변이 연결된 요청은 취소 처리할 수 없습니다.', + }); + } + + if (result.reason === 'active') { + return reply.code(409).send({ + message: '현재 처리 중인 요청은 여기서 취소 처리할 수 없습니다.', + }); + } + + if (result.reason === 'already_terminal') { + return reply.code(409).send({ + message: '이미 취소 처리된 요청입니다.', + }); + } + + return reply.code(404).send({ + message: '취소 처리할 요청을 찾지 못했습니다.', + }); + } + + getActiveChatService()?.broadcastRequestUpdate(tokenPayload.sessionId, result.item); + + return { + ok: true, + item: result.item, + }; + }); + + app.post(`${CHAT_SHARE_ROUTE_PREFIX}/:token/request-retry`, async (request, reply) => { + const params = z.object({ + token: z.string().trim().min(1).max(16000), + }).parse(request.params ?? {}); + const payload = z.object({ + parentRequestId: z.string().trim().min(1).max(120), + }).parse(request.body ?? {}); + const managedContext = await resolveManagedChatShareContext(params.token); + const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token); + + if (!tokenPayload) { + return reply.code(404).send({ + message: '공유 링크가 유효하지 않습니다.', + }); + } + + const shareSnapshot = await buildChatShareSnapshot(tokenPayload); + + if (!shareSnapshot) { + return reply.code(404).send({ + message: '공유 대상을 찾을 수 없습니다.', + }); + } + + const unavailableMessage = resolveManagedShareUnavailableMessage(managedContext.managedResource); + + if (unavailableMessage) { + return reply.code(403).send({ + message: unavailableMessage, + }); + } + + if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) { + return; + } + + if (managedContext.managedResource?.token.permissions && !managedContext.managedResource.token.permissions.includes('comment')) { + return reply.code(403).send({ + message: '이 공유 링크에는 요청 재처리 권한이 없습니다.', + }); + } + + const blockedReason = resolveShareBlockedReason( + shareSnapshot.requests, + resolveChatShareTokenSettingSnapshot(tokenPayload), + ); + + if (blockedReason) { + return reply.code(409).send({ + message: blockedReason, + }); + } + + const normalizedParentRequestId = resolveRecoveredShareParentRequestId( + shareSnapshot, + payload.parentRequestId, + [shareSnapshot.targetRequest.requestId], + ); + + if (!normalizedParentRequestId || !shareSnapshot.requests.some((request) => request.requestId.trim() === normalizedParentRequestId)) { + return reply.code(403).send({ + message: '이 공유 링크 범위를 벗어난 요청입니다.', + }); + } + + if (tokenPayload.kind === 'prompt' && normalizedParentRequestId !== tokenPayload.requestId) { + return reply.code(403).send({ + message: '이 공유 링크에서 허용되지 않은 요청입니다.', + }); + } + + const targetRequest = shareSnapshot.requests.find((request) => request.requestId.trim() === normalizedParentRequestId) ?? null; + + if ( + !targetRequest + || targetRequest.hasResponse + || targetRequest.status !== 'failed' + || (targetRequest.statusMessage?.trim() ?? '') !== '중단된 오래된 요청' + ) { + return reply.code(409).send({ + message: '지금은 이 요청을 재처리할 수 없습니다.', + }); + } + + const normalizedUserText = targetRequest.userText.trim(); + + if (!normalizedUserText) { + return reply.code(409).send({ + message: '재처리할 요청 본문이 없습니다.', + }); + } + + const queuedRequestId = await getActiveChatService()?.submitExternalMessage(tokenPayload.sessionId, normalizedUserText, { + mode: 'direct', + requestOrigin: targetRequest.requestOrigin === 'prompt' ? 'prompt' : 'composer', + sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null, + parentRequestId: targetRequest.requestOrigin === 'prompt' ? targetRequest.parentRequestId?.trim() || null : null, + clientId: targetRequest.requesterClientId ?? shareSnapshot.conversation?.clientId ?? null, + }); + + if (!queuedRequestId) { + return reply.code(503).send({ + message: '중단된 요청 재처리를 시작하지 못했습니다.', + }); + } + + if (managedContext.managedResource) { + await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, { + actorLabel: 'share-viewer', + summary: '공유 채팅에서 중단 요청 재처리를 시작했습니다.', + detail: normalizedParentRequestId, + }); + } + + return { + ok: true, + queuedRequestId, + }; + }); + app.get('/api/chat/conversations', async (request) => { const query = z.object({ limit: z.coerce.number().int().min(1).max(200).optional(), @@ -2622,6 +2971,7 @@ export async function registerChatRoutes(app: FastifyInstance) { }), ).max(20).optional(), summaryText: z.string().max(10000).optional().nullable(), + attachments: z.array(chatComposerAttachmentSchema).max(20).optional(), }).parse(request.body ?? {}); if (params.requestId !== payload.parentRequestId) { @@ -2675,6 +3025,7 @@ export async function registerChatRoutes(app: FastifyInstance) { }), ).max(20).optional(), summaryText: z.string().max(10000).optional().nullable(), + attachments: z.array(chatComposerAttachmentSchema).max(20).optional(), followupText: z.string().trim().min(1).max(20000), mode: z.enum(['queue', 'direct']).optional(), contextRef: chatPromptContextRefSchema, diff --git a/etc/servers/work-server/src/routes/health.ts b/etc/servers/work-server/src/routes/health.ts index 46b01d7..a050414 100644 --- a/etc/servers/work-server/src/routes/health.ts +++ b/etc/servers/work-server/src/routes/health.ts @@ -1,11 +1,28 @@ import type { FastifyInstance } from 'fastify'; +import { getActiveChatService } from '../services/chat-service.js'; +import { getRuntimeDrainSnapshot } from '../services/runtime-drain-service.js'; +import { getRuntimeWorkServerBuildInfo } from '../services/work-server-build-service.js'; export async function registerHealthRoutes(app: FastifyInstance) { - const respondHealth = async () => ({ - ok: true, - service: 'work-server', - timestamp: new Date().toISOString(), - }); + const respondHealth = async () => { + const buildInfo = getRuntimeWorkServerBuildInfo(); + const chatRuntimeSnapshot = getActiveChatService()?.getRuntimeSnapshot() ?? null; + + return { + ok: true, + service: 'work-server', + slot: process.env.WORK_SERVER_SLOT?.trim() || null, + timestamp: new Date().toISOString(), + buildId: buildInfo?.buildId ?? null, + builtAt: buildInfo?.builtAt ?? null, + ...getRuntimeDrainSnapshot(), + activeChatRequestCount: chatRuntimeSnapshot?.activeRequestCount ?? 0, + queuedChatRequestCount: chatRuntimeSnapshot?.queuedRequestCount ?? 0, + connectedChatSessionCount: chatRuntimeSnapshot?.connectedSessionCount ?? 0, + activeChatSocketCount: chatRuntimeSnapshot?.activeSocketCount ?? 0, + canAcceptNewChatRequests: chatRuntimeSnapshot?.canAcceptNewRequests ?? true, + }; + }; app.get('/', respondHealth); app.get('/api', respondHealth); diff --git a/etc/servers/work-server/src/routes/runtime.ts b/etc/servers/work-server/src/routes/runtime.ts new file mode 100644 index 0000000..6b97c95 --- /dev/null +++ b/etc/servers/work-server/src/routes/runtime.ts @@ -0,0 +1,42 @@ +import type { FastifyInstance } from 'fastify'; +import { z } from 'zod'; +import { getActiveChatService } from '../services/chat-service.js'; +import { + beginRuntimeDrain, + endRuntimeDrain, + getRuntimeDrainSnapshot, +} from '../services/runtime-drain-service.js'; + +const runtimeDrainBodySchema = z.object({ + draining: z.boolean(), +}); + +function buildRuntimeResponse() { + const chatRuntimeSnapshot = getActiveChatService()?.getRuntimeSnapshot() ?? null; + + return { + ok: true, + ...getRuntimeDrainSnapshot(), + activeChatRequestCount: chatRuntimeSnapshot?.activeRequestCount ?? 0, + queuedChatRequestCount: chatRuntimeSnapshot?.queuedRequestCount ?? 0, + connectedChatSessionCount: chatRuntimeSnapshot?.connectedSessionCount ?? 0, + activeChatSocketCount: chatRuntimeSnapshot?.activeSocketCount ?? 0, + canAcceptNewChatRequests: chatRuntimeSnapshot?.canAcceptNewRequests ?? true, + }; +} + +export async function registerRuntimeRoutes(app: FastifyInstance) { + app.get('/api/runtime', async () => buildRuntimeResponse()); + + app.post('/api/runtime/drain', async (request) => { + const { draining } = runtimeDrainBodySchema.parse(request.body ?? {}); + + if (draining) { + beginRuntimeDrain(); + } else { + endRuntimeDrain(); + } + + return buildRuntimeResponse(); + }); +} diff --git a/etc/servers/work-server/src/routes/server-command.ts b/etc/servers/work-server/src/routes/server-command.ts index dacf0cd..c932cda 100644 --- a/etc/servers/work-server/src/routes/server-command.ts +++ b/etc/servers/work-server/src/routes/server-command.ts @@ -1,6 +1,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { z } from 'zod'; import { env } from '../config/env.js'; +import { getSharedResourceTokenDetailBySharePath } from '../services/shared-resource-token-service.js'; import { listServerCommands, restartServerCommand, serverCommandKeys } from '../services/server-command-service.js'; import { cancelServerRestartReservation, @@ -16,8 +17,10 @@ const serverCommandParamSchema = z.object({ }); const restartReservationBodySchema = z.object({ + target: z.enum(['all', 'test', 'work-server']).optional(), autoExecuteDelaySeconds: z.number().int().min(1).max(300).optional(), }); +const CHAT_SHARE_PATH_PREFIX = '/chat/share/'; function getImmediateRestartBlockInfo( key: z.infer['key'], @@ -60,6 +63,39 @@ function getRequestAccessToken(request: FastifyRequest) { return Array.isArray(tokenHeader) ? tokenHeader[0]?.trim() ?? '' : String(tokenHeader ?? '').trim(); } +function getRequestChatShareToken(request: FastifyRequest) { + const tokenHeader = request.headers['x-chat-share-token']; + return Array.isArray(tokenHeader) ? tokenHeader[0]?.trim() ?? '' : String(tokenHeader ?? '').trim(); +} + +function resolveChatSharePath(token: string) { + return `${CHAT_SHARE_PATH_PREFIX}${encodeURIComponent(token)}`; +} + +async function resolveSharedServerCommandAccessContext(request: FastifyRequest) { + const shareToken = getRequestChatShareToken(request); + if (!shareToken) { + return null; + } + + const managedResource = await getSharedResourceTokenDetailBySharePath(resolveChatSharePath(shareToken)); + if (!managedResource || managedResource.token.enabled === false || managedResource.token.revokedAt) { + return null; + } + + const allowedAppIds = managedResource.token.allowedAppIds ?? []; + const normalizedAllowedAppIds = new Set(allowedAppIds.map((item) => item.trim().toLowerCase()).filter(Boolean)); + + if (!managedResource.token.permissions.includes('manage') || !normalizedAllowedAppIds.has('server-command')) { + return null; + } + + return { + scope: 'shared' as const, + allowedKeys: new Set(['work-server']), + }; +} + function getRequestClientId(request: FastifyRequest) { const clientIdHeader = request.headers['x-client-id']; return Array.isArray(clientIdHeader) ? clientIdHeader[0]?.trim() ?? '' : String(clientIdHeader ?? '').trim(); @@ -78,36 +114,48 @@ function getRequestAppOrigin(request: FastifyRequest) { return origin?.trim() ?? ''; } -function ensureAuthorized(request: FastifyRequest, reply: FastifyReply) { +async function resolveServerCommandAccessContext(request: FastifyRequest) { if (getRequestAccessToken(request) === env.SERVER_COMMAND_ACCESS_TOKEN) { - return true; + return { scope: 'full' as const }; } + return resolveSharedServerCommandAccessContext(request); +} + +function sendAccessDenied(reply: FastifyReply) { reply.status(403); void reply.send({ - message: '권한 토큰이 필요합니다.', + message: '권한 토큰 또는 워크서버 재기동 권한이 있는 공유채팅 링크가 필요합니다.', }); - return false; } export async function registerServerCommandRoutes(app: FastifyInstance) { app.get('/api/server-commands', async (request, reply) => { - if (!ensureAuthorized(request, reply)) { + const accessContext = await resolveServerCommandAccessContext(request); + if (!accessContext) { + sendAccessDenied(reply); return; } + const items = await listServerCommands(); return { ok: true, - items: await listServerCommands(), + items: accessContext.scope === 'full' ? items : items.filter((item) => accessContext.allowedKeys.has(item.key)), }; }); app.post('/api/server-commands/:key/actions/restart', async (request, reply) => { - if (!ensureAuthorized(request, reply)) { + const accessContext = await resolveServerCommandAccessContext(request); + if (!accessContext) { + sendAccessDenied(reply); return; } const { key } = serverCommandParamSchema.parse(request.params); + if (accessContext.scope !== 'full' && !accessContext.allowedKeys.has(key)) { + reply.status(403); + return { ok: false, message: '현재 공유채팅 링크로는 이 서버를 재기동할 수 없습니다.' }; + } if (key === 'test' || key === 'work-server') { const workloadSummary = await getRestartReservationWorkloadSummary(); @@ -160,7 +208,9 @@ export async function registerServerCommandRoutes(app: FastifyInstance) { }); app.get('/api/server-commands/restart-reservation', async (request, reply) => { - if (!ensureAuthorized(request, reply)) { + const accessContext = await resolveServerCommandAccessContext(request); + if (!accessContext) { + sendAccessDenied(reply); return; } @@ -171,7 +221,9 @@ export async function registerServerCommandRoutes(app: FastifyInstance) { }); app.put('/api/server-commands/restart-reservation', async (request, reply) => { - if (!ensureAuthorized(request, reply)) { + const accessContext = await resolveServerCommandAccessContext(request); + if (!accessContext) { + sendAccessDenied(reply); return; } @@ -187,9 +239,14 @@ export async function registerServerCommandRoutes(app: FastifyInstance) { const parsed = restartReservationBodySchema.parse(payload ?? {}); + if (accessContext.scope !== 'full' && parsed.target !== 'work-server') { + return reply.status(403).send({ message: '현재 공유채팅 링크로는 WORK 서버 재기동 예약만 사용할 수 있습니다.' }); + } + return { ok: true, item: await scheduleServerRestartReservation({ + target: parsed.target, clientId: getRequestClientId(request), appOrigin: getRequestAppOrigin(request), autoExecuteDelaySeconds: parsed.autoExecuteDelaySeconds, @@ -198,7 +255,9 @@ export async function registerServerCommandRoutes(app: FastifyInstance) { }); app.post('/api/server-commands/restart-reservation/confirm', async (request, reply) => { - if (!ensureAuthorized(request, reply)) { + const accessContext = await resolveServerCommandAccessContext(request); + if (!accessContext) { + sendAccessDenied(reply); return; } @@ -209,7 +268,9 @@ export async function registerServerCommandRoutes(app: FastifyInstance) { }); app.delete('/api/server-commands/restart-reservation', async (request, reply) => { - if (!ensureAuthorized(request, reply)) { + const accessContext = await resolveServerCommandAccessContext(request); + if (!accessContext) { + sendAccessDenied(reply); return; } diff --git a/etc/servers/work-server/src/server.ts b/etc/servers/work-server/src/server.ts index 08cee39..d063c88 100644 --- a/etc/servers/work-server/src/server.ts +++ b/etc/servers/work-server/src/server.ts @@ -5,10 +5,12 @@ import { ChatService } from './services/chat-service.js'; import { ensureChatConversationTables } from './services/chat-room-service.js'; import { shutdownNotificationProvider } from './services/notification-service.js'; import { ServerRestartReservationWorker } from './services/server-restart-reservation-service.js'; +import { BaseballTicketBayWorker } from './workers/baseball-ticket-bay-worker.js'; import { PlanWorker } from './workers/plan-worker.js'; const app = createApp(); const planWorker = new PlanWorker(app.log); +const baseballTicketBayWorker = new BaseballTicketBayWorker(app.log); const serverRestartReservationWorker = new ServerRestartReservationWorker(app.log); const chatService = new ChatService(app.log); const startedAt = Date.now(); @@ -24,6 +26,7 @@ async function start() { port: env.PORT, }); planWorker.start(); + baseballTicketBayWorker.start(); serverRestartReservationWorker.start(); } catch (error) { app.log.error(error); @@ -46,6 +49,7 @@ async function shutdown(signal: string) { try { await planWorker.stop(); + await baseballTicketBayWorker.stop(); await serverRestartReservationWorker.stop(); chatService.close(); await app.close(); diff --git a/etc/servers/work-server/src/services/baseball-ticket-bay-service.ts b/etc/servers/work-server/src/services/baseball-ticket-bay-service.ts new file mode 100644 index 0000000..f4f3d62 --- /dev/null +++ b/etc/servers/work-server/src/services/baseball-ticket-bay-service.ts @@ -0,0 +1,1449 @@ +import { z } from 'zod'; +import { db } from '../db/client.js'; +import { sendNotifications } from './notification-service.js'; + +const TICKET_BAY_ORIGIN = 'https://www.ticketbay.co.kr'; +const SPORTS_SEARCH_KEY = '5'; +const MAX_CATEGORY_COUNT = 16; +const MAX_RESULT_COUNT = 12; +const TICKET_BAY_FETCH_TIMEOUT_MS = 12_000; +const TICKET_BAY_PRODUCT_PAGE_SIZE = 100; + +const teamKeywordMap: Record = { + LG: 'LG', + 두산: '두산', + SSG: 'SSG', + 키움: '키움', + KT: 'KT', + KIA: 'KIA', + NC: 'NC', + 롯데: '롯데', + 삼성: '삼성', + 한화: '한화', + 전체: '야구', +}; + +const titleSearchKeywordEntries = [ + '잠실', + '고척', + '광주', + '대구', + '대전', + '사직', + '수원', + '창원', + '인천', + '문학', + 'LG', + '두산', + 'SSG', + '키움', + 'KT', + 'KIA', + 'NC', + '롯데', + '삼성', + '한화', +] as const; + +const ticketAlertSearchSchema = z.object({ + title: z.string().trim().min(1).max(200), + eventDate: z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/), + team: z.string().trim().min(1).max(50), + zone: z.string().trim().min(1).max(100), + aisleSide: z.string().trim().min(1).max(100), + seatDirections: z.array(z.string().trim().min(1).max(50)).max(10).default([]), + maxPrice: z.number().finite().positive().nullable(), + seatCount: z.number().int().positive().max(10), +}); + +type TicketAlertSearchInput = z.infer; + +type AlertMatchFailureReason = + | 'status' + | 'eventDate' + | 'team' + | 'maxPrice' + | 'seatCount' + | 'zone' + | 'aisleSide' + | 'seatDirections'; + +type AlertMatchEvaluation = + | { matched: true } + | { + matched: false; + failedReason: AlertMatchFailureReason; + }; + +type SearchCategoryItem = { + depth1Id?: number; + depth2Id?: number; + depth3Id?: number; + depth1Name?: string; + depth2Name?: string; + depth3Name?: string; + startDate?: string | null; + endDate?: string | null; + performCity?: string | null; + performLocation?: string | null; +}; + +type TicketBayProductListItem = { + id: number; + category_id: number; + display_number: string; + name: string; + start_perform_date: string | null; + end_perform_date: string | null; + depth1_name?: string | null; + depth2_name?: string | null; + depth3_name?: string | null; + info_type?: string | null; + seat_number_type?: string | null; + seat_number?: string | null; + area?: string | null; + floor?: string | null; + grade?: string | null; + addinfo?: string | null; + sale_quantity?: number | null; + is_together?: string | null; + price?: number | null; + total_price?: number | null; + product_status?: string | null; + is_use_safe?: string | null; + is_use_pin?: string | null; + is_use_delivery?: string | null; + is_use_field?: string | null; + is_use_etc?: string | null; + description?: string | null; + created_at?: string | null; + transaction_type?: string | null; +}; + +type TicketBayProductDetailInfo = TicketBayProductListItem & { + product_remark?: Array<{ id?: number; content?: string | null }>; + seat_remark?: Array<{ id?: number; content?: string | null }>; + photo_res?: Array<{ photo_url?: string | null; origin_name?: string | null }>; + transaction_type?: string | null; +}; + +type TicketBayCategoryInfo = { + seat_image?: string | null; +}; + +const CATEGORY_VARIANT_SUFFIX_PATTERN = /\s*\((?:주중|금토일\/공휴일)\)\s*$/u; + +function normalizeText(value: unknown) { + return String(value ?? '').trim(); +} + +function normalizeDateTime(value: unknown) { + if (value instanceof Date) { + return value.toISOString(); + } + + const normalized = normalizeText(value); + + if (!normalized) { + return ''; + } + + const parsed = Date.parse(normalized); + return Number.isNaN(parsed) ? normalized : new Date(parsed).toISOString(); +} + +function normalizeTimestampForDb(value: unknown) { + if (value instanceof Date) { + return value.toISOString(); + } + + const normalized = normalizeText(value); + + if (!normalized) { + return null; + } + + const candidates = [ + normalized, + normalized.replace(/\s*\([^)]*\)\s*$/u, ''), + normalized.replace(/\s*\([^)]*\)\s*$/u, '').replace(/\s+GMT([+-]\d{4})$/iu, ' $1'), + ]; + + for (const candidate of candidates) { + const parsed = Date.parse(candidate); + + if (!Number.isNaN(parsed)) { + return new Date(parsed).toISOString(); + } + + const offsetMatch = candidate.match( + /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\s*([+-]\d{2})(\d{2})$/u, + ); + + if (offsetMatch) { + const [, datePart, timePart, hourOffset, minuteOffset] = offsetMatch; + const reparsed = Date.parse(`${datePart}T${timePart}${hourOffset}:${minuteOffset}`); + + if (!Number.isNaN(reparsed)) { + return new Date(reparsed).toISOString(); + } + } + } + + return null; +} + +function normalizeDateOnly(value: unknown) { + const normalized = normalizeText(value); + + if (!normalized) { + return ''; + } + + return normalized.slice(0, 10); +} + +function buildCategorySearchKeywords(input: TicketAlertSearchInput) { + const keywords: string[] = []; + const primaryKeyword = teamKeywordMap[input.team] ?? normalizeText(input.team); + + if (primaryKeyword) { + keywords.push(primaryKeyword); + } + + const title = normalizeText(input.title); + + if (title) { + titleSearchKeywordEntries.forEach((keyword) => { + if (title.includes(keyword)) { + keywords.push(keyword); + } + }); + } + + return [...new Set(keywords.filter(Boolean))]; +} + +function resolveCategoryFamilyKey(item: SearchCategoryItem) { + const label = normalizeText(item.depth3Name || item.depth2Name || String(item.depth3Id)); + return label.replace(CATEGORY_VARIANT_SUFFIX_PATTERN, '').trim(); +} + +function expandCategoryVariants(categories: SearchCategoryItem[]) { + const familyMap = new Map(); + + categories.forEach((item) => { + const familyKey = resolveCategoryFamilyKey(item); + const existing = familyMap.get(familyKey); + + if (existing) { + existing.push(item); + return; + } + + familyMap.set(familyKey, [item]); + }); + + const expanded: SearchCategoryItem[] = []; + const seenCategoryIds = new Set(); + + categories.forEach((item) => { + const familyItems = familyMap.get(resolveCategoryFamilyKey(item)) ?? [item]; + + familyItems.forEach((familyItem) => { + if (typeof familyItem.depth3Id !== 'number' || seenCategoryIds.has(familyItem.depth3Id)) { + return; + } + + seenCategoryIds.add(familyItem.depth3Id); + expanded.push(familyItem); + }); + }); + + return expanded; +} + +async function fetchText(url: string) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TICKET_BAY_FETCH_TIMEOUT_MS); + + let response: Response; + + try { + response = await fetch(url, { + signal: controller.signal, + headers: { + 'user-agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', + accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + }, + }); + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + throw new Error(`티켓베이 요청 시간 초과(${TICKET_BAY_FETCH_TIMEOUT_MS}ms): ${url}`); + } + + throw error; + } finally { + clearTimeout(timeout); + } + + if (!response.ok) { + throw new Error(`티켓베이 요청 실패: ${response.status} ${url}`); + } + + return response.text(); +} + +async function fetchJson(url: string, init?: RequestInit) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TICKET_BAY_FETCH_TIMEOUT_MS); + + let response: Response; + + try { + response = await fetch(url, { + ...init, + signal: controller.signal, + headers: { + 'user-agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', + accept: 'application/json, text/plain, */*', + 'content-type': 'application/json', + ...(init?.headers ?? {}), + }, + }); + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + throw new Error(`티켓베이 요청 시간 초과(${TICKET_BAY_FETCH_TIMEOUT_MS}ms): ${url}`); + } + + throw error; + } finally { + clearTimeout(timeout); + } + + if (!response.ok) { + throw new Error(`티켓베이 요청 실패: ${response.status} ${url}`); + } + + return (await response.json()) as T; +} + +function parseNextDataJson(html: string) { + const match = html.match(/