Máy co màng cầm tay thực chất là một thiết bị khò nhiệt. Dây mayso được làm nóng để tạo luồng khí nóng, sau đó tác động lên màng co và làm lớp màng ôm sát sản phẩm. Về thông số tham khảo, máy thường có công suất 1500–2000W, nhiệt độ khoảng 50–600°C, lưu lượng gió 250–500 lít/phút và trọng lượng 0,6–1,2kg. 1. Vì sao cơ sở nhỏ thường sử dụng? Chi phí đầu tư thấp là một ưu điểm đáng chú ý. Một máy có thể có giá từ vài trăm nghìn đến khoảng 2 - 3 triệu đồng tùy thương hiệu và tính năng. Thiết kế dạng súng cũng giúp người dùng xử lý nhiều loại sản phẩm mà không bị giới hạn bởi kích thước buồng. Đây là điểm hữu ích khi đóng gói giỏ quà, hàng cồng kềnh hoặc sản phẩm có hình dạng đặc biệt. 2. Vấn đề nằm ở năng suất và độ đồng đều Máy thủ công phụ thuộc trực tiếp vào tốc độ và kỹ năng của người vận hành. Năng suất tham khảo khoảng 100 - 300 sản phẩm/giờ. Nếu nhiệt đưa vào không đều, màng có thể nhăn, cháy, thủng hoặc rách. Vì vậy, thiết bị phù hợp hơn với cơ sở có lượng hàng vừa phải. Khi cần đóng gói hàng nghìn sản phẩm, nên cân nhắc các dòng máy buồng hoặc hệ thống có mức tự động hóa cao hơn. 👉 Xem thêm phân tích đầy đủ: Ưu nhược điểm máy co màng cầm tay thủ công
2026.09.30 문제 풀이 1차 실행 오류 86.9/100.0 런타임 에러 런타임 에러 원인 분석 int 가 표현할 수 있는 값의 범위를 넘어가는 경우가 발생한다. class Solution { private boolean isPrime(int n) { if (n <= 1) { return false; } for (int i = 2; i <= Math.sqrt(n); i++) { if (n % i == 0) { return false; } } return true; } public int solution(int n, int k) { int cnt = 0; String s = Integer.toString(n, k); StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '0') { if (isPrime(Integer.parseInt(sb.toString()))) { cnt++; } sb.setLength(0); } sb.append(c); } if (!sb.isEmpty() && isPrime(Integer.parseInt(sb.toString()))) { cnt++; } return cnt; } } 나의 코드 소요 시간: 32분 시간 복잡도: $O(√V)$ class Solution { private boolean isPrime(long n) { if (n <= 1) { return false; } if (n == 2) { return true; } for (int i = 2; i <= Math.sqrt(n); i++) { if (n % i == 0) { return false; } } return true; } public int solution(int n, int k) { int cnt = 0; String s = Integer.toString(n, k); StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '0') { if (!sb.isEmpty() && isPrime(Long.parseLong(sb.toString()))) { cnt++; } sb.setLength(0); } else { sb.append(c); } } if (!sb.isEmpty() && isPrime(Long.parseLong(sb.toString()))) { cnt++; } sb.setLength(0); return cnt; } } AI 코드 시간 복잡도: $O(√V)$ class Solution { public int solution(int n, int k) { int cnt = 0; for (String part : Integer.toString(n, k).split("0")) { if (!part.isEmpty() && isPrime(Long.parseLong(part))) cnt++; } return cnt; } private boolean isPrime(long v) { if (v < 2) return false; if (v < 4) return true; // 2, 3 if (v % 2 == 0 || v % 3 == 0) return false; // 6k±1 형태만 검사 (2, 3의 배수는 이미 걸러짐) for (long i = 5; i * i <= v; i += 6) { if (v % i == 0 || v % (i + 2) == 0) return false; } return true; } } 문제 풀이 후기 실행을 하면서 런타임 오류를 잡는 데에 실패했다. int 의 표현 범위 오류도 고려했으나, n <= 1,000,000 의 범위를 가지기에 범위 초과 오류가 일어날 것이라고 생각하지 못했다. 코드를 주석 처리해가며 런타임 오류가 발생한 부분은 찾아냈으나, 왜 발생하는지를 알아차리지 못했다.
현대중공업 글에서는 백엔드가 쓴 API 명세를 읽고 화면을 붙인 이야기를 했다. 그 글에서 나는 API를 쓰는 쪽이었다. 명세가 먼저 있었고, 나는 그 계약대로 요청을 보내고 응답을 그렸다. 유라와 KT는 위치가 달랐다. 유라에서는 회원 기능 API의 요청과 응답 형태, 에러 코드까지 내가 정하고 화면도 내가 붙였다. 만드는 쪽과 쓰는 쪽을 한 사람이 다 쥔 경우다. KT에서는 이미 돌아가던 조회 API의 계약을 성능 때문에 바꿨다. 응답 필드를 빼고 요청 필드를 넣고 선택 항목을 필수로 만드는 일이었고, 화면과 서버를 같은 배포에 맞춰야 했다. 세 경험을 한 줄씩 놓으면 이렇다. 회사 내 위치 명세는 어디에 계약을 정한 사람 현대중공업 명세를 읽고 화면을 붙였다 Confluence, 백엔드가 작성 백엔드 유라 서버와 화면을 다 만들었다 GitLab 위키, 내가 작성 나 KT 화면 쪽 API를 만들고 계약을 바꿨다 화면 정의서와 요청관리 시스템 기획과 내가 같이 이 글은 아래 두 줄을 채우는 글이다. 코드와 URL과 필드 이름은 구조를 보이기 위해 다시 적은 것이라 실제 이름과 글자 단위로 같지는 않다. 1. 유라: 화면 하나에 API가 서너 개 1-1. JSP인데 왜 API 기반이 됐나 환경은 전자정부프레임워크에 JSP와 jQuery, DB는 PostgreSQL이었다. 겉으로는 서버가 HTML을 만들어 내려주는 전통적인 구조로 보인다. 그런데 회원 기능은 JSP가 껍데기만 그리고 데이터는 전부 Ajax로 받는 구조였다. 이유는 인증 방식 하나에서 나왔다. 로그인 결과로 JWT를 발급해 브라우저 localStorage에 두고, 요청마다 Authorization: Bearer 헤더에 실어 보내는 방식이었다. 세션도 쿠키도 없다. 그러면 JSP를 렌더링하는 시점에 서버는 누가 왔는지 모른다. 토큰은 브라우저의 JS만 읽을 수 있고, 주소창으로 들어오는 페이지 요청에는 그 헤더가 붙지 않기 때문이다. 서버가 JSP 안에 사용자 데이터를 박아 넣을 수 없으니, 데이터는 토큰을 실어 보낼 수 있는 Ajax로만 받게 된다. 그래서 화면 하나가 열리는 순서가 이렇다. JSP가 빈 표와 버튼을 그린다 => 페이지 로드 시 JS가 localStorage의 토큰을 확인하고 없거나 만료면 로그인으로 보낸다 => 목록 API를 Bearer 헤더를 붙여 부른다 => 응답 JSON으로 표를 채운다. 관리자 회원 목록 화면 하나만 봐도 목록 조회, 상세 조회, 승인, 반려 네 개의 API가 붙는다. JSP 프로젝트였지만 회원 기능은 사실상 API 기반으로 만든 셈이다. 임직원 SSO는 유라 IT가 이미 쓰던 방식이라 내가 건드리지 않았다. 이 글에 적은 것은 사내 계정이 없는 협력사와 해외법인 사용자, 즉 외부 사용자 계정 쪽이다. 1-2. 내가 만든 엔드포인트 회원 기능은 controller, service, Mapper, SQL까지 내가 전담했다. 정리하면 이런 목록이다. 기능 메서드와 URL 권한 비고 인증번호 발송 POST /api/auth/email-codes 공개 60초 쿨다운, 재발송하면 시도 횟수 초기화 인증번호 검증 POST /api/auth/email-codes/verify 공개 5분 만료, 5회 제한 이메일 가입 POST /api/members 공개 서버가 인증번호를 다시 검증하고 승인 대기 상태로 생성 로그인 POST /api/auth/login 공개 5회 실패 잠금, 최신 토큰 저장, 로그인 이력 기록 소셜 로그인 POST /api/auth/social/{provider} 공개 프론트가 받은 소셜 토큰을 서버가 검증. 신규면 임시 계정 생성, 휴면이면 자동 해제 추가정보 입력 PUT /api/members/me/additional-info 로그인 (임시 계정) 소셜 가입자의 사업자번호와 담당자 정보 비밀번호 재설정 요청 POST /api/auth/password-reset/request 공개 등록 이메일로 인증번호 비밀번호 재설정 확정 POST /api/auth/password-reset/confirm 공개 본인이 새 비밀번호 설정, 잠금도 함께 해제 휴면 해제 POST /api/auth/reactivate 공개 이메일 계정용. 인증번호 확인 후 ACTIVE로 내 정보 조회와 수정 GET, PUT /api/members/me 로그인 탈퇴 DELETE /api/members/me 로그인 즉시 로그인 차단, 30일 후 배치가 마스킹 약관 조회 GET /api/terms 로그인 최신 버전과 재동의 필요 여부 약관 동의 POST /api/members/me/agreements 로그인 UPDATE가 아니라 INSERT, 버전별 이력 관리자 회원 목록 GET /api/admin/members 관리자 상태 필터, 서버 페이징 관리자 회원 상세 GET /api/admin/members/{memberNo} 관리자 로그인 이력 포함 승인, 반려, 잠금 해제, 휴면 해제, 탈퇴 처리 POST /api/admin/members/{memberNo}/approve 외 4개 관리자 반려는 사유 필수, 사유가 메일로 나간다 가입 폼의 사업자번호는 폼 안에서 형식을 확인했고, 실제로 그 회사 사람인지는 담당 부서가 승인 단계에서 봤다. 해외법인을 고르면 사업자번호 대신 법인 목록이 나오고 검증 대신 승인자가 달라진다. 1-3. 응답 봉투와 에러 코드를 먼저 정했다 이 목록을 만들기 전에 정한 것이 둘 있다. 응답의 겉모양과 에러 코드다. 화면 열 개가 API 스무 개를 부르는데 응답 모양이 API마다 다르면 화면마다 파싱 코드가 달라진다. 그래서 성공이든 실패든 같은 봉투에 담았다. { "success": true, "data": { "memberNo": 1042, "status": "PENDING_APPROVAL" }, "error": null } { "success": false, "data": null, "error": { "code": "TOKEN_MISMATCH", "message": "다른 기기에서 로그인되었습니다." } } HTTP 상태 코드만으로는 화면이 무엇을 해야 하는지 모른다. 401 하나 안에 토큰이 없는 경우, 만료된 경우, 다른 기기에서 로그인해 최신 토큰이 아닌 경우가 다 들어가는데 화면의 반응은 각각 다르다. 그래서 상태 코드는 크게 가르고, 화면이 분기할 이유는 error.code 에 담았다. HTTP code 뜻 화면이 하는 일 401 TOKEN_MISSING 토큰이 없다 로그인 화면으로 401 TOKEN_INVALID 서명이 맞지 않는다 토큰을 지우고 로그인 화면으로 401 TOKEN_EXPIRED 8시간이 지났다 로그인 화면으로, "다시 로그인해 주세요" 401 TOKEN_MISMATCH 최신 토큰이 아니다 강제 로그아웃, "다른 기기에서 로그인되었습니다" 401 LOGIN_FAILED 비밀번호 불일치 detail.remaining 으로 남은 횟수 표시 403 ACCOUNT_LOCKED 5회 실패로 잠김 잠금 안내와 비밀번호 찾기 링크 403 ACCOUNT_DORMANT 12개월 미접속 휴면 휴면 해제 화면으로 403 ACCOUNT_REJECTED 승인 반려 반려 사유 표시 403 NOT_APPROVED 승인 전 계정이 업무 API를 불렀다 승인 대기 화면으로 403 FORBIDDEN 권한 없음 접근 불가 안내 400 VALIDATION_FAILED 입력값 오류 detail.fields 로 항목별 표시 400 SOCIAL_ACCOUNT 소셜로 가입된 이메일로 비밀번호 로그인 시도 "카카오로 가입된 계정입니다" 안내 400 CODE_MISMATCH, CODE_EXPIRED, CODE_ATTEMPTS_EXCEEDED 인증번호 문제 각각 다른 문구, 마지막은 재발송 유도 409 DUPLICATE_EMAIL 이미 가입된 이메일 로그인 유도 429 CODE_COOLDOWN 60초 안에 재발송 요청 카운트다운 표시 502 MAIL_SEND_FAILED 사내 메일 서버 실패 "잠시 후 다시 시도" 안내 계정 상태도 코드로 정했다. 로그인 응답의 member.status 가 이 값 중 하나고, 화면은 이 값으로 첫 화면을 고른다. status 뜻 로그인하면 PENDING_INFO 소셜 가입 직후, 추가정보 입력 전 토큰 발급, 추가정보 화면으로 PENDING_APPROVAL 승인 대기 토큰 발급, 대기 화면만 보인다 ACTIVE 승인 완료 정상 REJECTED 반려 403 ACCOUNT_REJECTED LOCKED 5회 실패 잠금 403 ACCOUNT_LOCKED DORMANT 12개월 미접속 403 ACCOUNT_DORMANT WITHDRAWN 탈퇴 로그인 차단 승인 전에도 로그인이 되게 한 이유는 문의를 줄이기 위해서다. 로그인을 막으면 본인이 승인 대기 중인지 비밀번호를 틀린 것인지 구분을 못 한다. 로그인은 되게 하고 대기 중이라는 것을 화면으로 보여주는 쪽이 문의가 덜 온다. 대신 승인 전 계정이 업무 API를 부르면 서버가 NOT_APPROVED를 내린다. 메뉴만 숨긴 것이 아니라 서버가 막는다. 로그인 응답은 이렇게 생겼다. { "success": true, "data": { "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "expiresIn": 28800, "member": { "memberNo": 1042, "name": "김담당", "role": "PARTNER", "status": "ACTIVE", "passwordChangeRequired": false, "termsReagreeRequired": true } }, "error": null } passwordChangeRequired 는 관리자가 임시 비밀번호로 발급한 계정의 첫 로그인을 비밀번호 변경 화면으로 보내기 위한 것이고, termsReagreeRequired 는 약관이 개정돼 재동의가 필요하면 팝업을 띄우기 위한 것이다. 둘 다 서버가 판단하고 화면은 값만 본다. 에러 코드 표가 있으면 화면 쪽 공통 처리가 한 곳에 모인다. jQuery의 전역 Ajax 설정에 토큰을 붙이는 일과 에러 코드를 처리하는 일을 넣었다. // common/api.js (function () { var TOKEN_KEY = 'accessToken'; function getToken() { return localStorage.getItem(TOKEN_KEY); } // 만료 선검사. 서명 검증이 아니라 디코딩만이다. // 만료 토큰으로 401을 받는 왕복을 줄이는 용도고, 진짜 검증은 서버 필터가 한다. function isExpired(token) { try { var payload = jwt_decode(token); return payload.exp * 1000 <= Date.now(); // exp는 초 단위라 1000을 곱한다 } catch (e) { return true; } } function forceLogout(message) { localStorage.removeItem(TOKEN_KEY); if (message) alert(message); location.href = '/login.do'; } $.ajaxSetup({ beforeSend: function (xhr, settings) { if (settings.skipAuth) return; // 로그인, 가입 같은 공개 API var token = getToken(); if (!token || isExpired(token)) { forceLogout('로그인이 필요합니다.'); return false; // 요청 자체를 보내지 않는다 } xhr.setRequestHeader('Authorization', 'Bearer ' + token); } }); // 모든 Ajax 실패가 한 번 거쳐 가는 자리 $(document).ajaxError(function (event, xhr, settings) { if (settings.skipGlobalError) return; var body = xhr.responseJSON || {}; var code = body.error && body.error.code; switch (code) { case 'TOKEN_MISSING': case 'TOKEN_INVALID': case 'TOKEN_EXPIRED': forceLogout('로그인이 만료되었습니다. 다시 로그인해 주세요.'); break; case 'TOKEN_MISMATCH': forceLogout('다른 기기에서 로그인되었습니다.'); break; case 'NOT_APPROVED': location.href = '/member/pending.do'; break; case 'ACCOUNT_DORMANT': location.href = '/member/reactivate.do'; break; default: // 400대 검증 오류는 화면별 처리에 맡기고, 여기서는 서버 오류 공통 문구만 띄운다 if (xhr.status >= 500) alert('일시적인 오류입니다. 잠시 후 다시 시도해 주세요.'); } }); })(); 화면마다 .fail() 에서 토큰 만료를 처리했다면 화면 수만큼 같은 코드가 생겼을 것이다. 코드 표를 먼저 정한 덕에 이 파일 하나로 끝났고, 나중에 "다른 기기에서 로그인되었습니다" 문의가 1위로 올라왔을 때도 고칠 곳이 한 곳이었다. 1-4. 서버 필터: 세 가지 검증과 URL별 권한 서버 쪽은 필터 하나가 문지기다. 순서는 토큰 파싱 => 서명 검증 => 만료 검증 => DB의 최신 토큰과 비교 => URL 권한 확인이다. 앞의 둘은 토큰만 보면 되고 세 번째만 DB를 본다. JWT인데 매 요청 DB를 보면 무상태의 의미가 줄어드는 것은 맞다. 다른 기기 로그인을 끊는 것이 요구사항이어서 최신 토큰 비교 하나만큼은 서버 상태를 뒀다. public class JwtAuthFilter implements Filter { private final JwtProvider jwtProvider; // 서명 검증, 만료 검증, 클레임 추출 private final MemberMapper memberMapper; // 최신 토큰, 역할, 상태 조회 private final UrlPolicy urlPolicy; // URL별 권한 규칙 @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String path = request.getRequestURI(); if (urlPolicy.isPublic(path)) { // 로그인, 가입, 인증번호, 비밀번호 찾기 chain.doFilter(req, res); return; } String token = resolveBearer(request.getHeader("Authorization")); if (token == null) { ApiError.write(response, 401, "TOKEN_MISSING", "로그인이 필요합니다."); return; } Claims claims; try { claims = jwtProvider.parse(token); // 1) 서명과 2) 만료를 여기서 함께 검증한다 } catch (ExpiredJwtException e) { ApiError.write(response, 401, "TOKEN_EXPIRED", "로그인이 만료되었습니다."); return; } catch (JwtException e) { ApiError.write(response, 401, "TOKEN_INVALID", "유효하지 않은 토큰입니다."); return; } Long memberNo = claims.get("memberNo", Long.class); // 최신 토큰을 보러 DB에 가는 길에 역할과 상태도 같이 읽는다. // 상태는 승인 직후 바로 반영돼야 하니 토큰 클레임이 아니라 DB 값을 쓴다. MemberAuth auth = memberMapper.selectAuthInfo(memberNo); if (auth == null || !token.equals(auth.getLatestToken())) { // 3) 최신 토큰인가 ApiError.write(response, 401, "TOKEN_MISMATCH", "다른 기기에서 로그인되었습니다."); return; } if (urlPolicy.isAdmin(path) && !"ADMIN".equals(auth.getRole())) { ApiError.write(response, 403, "FORBIDDEN", "권한이 없습니다."); return; } if (urlPolicy.requiresActive(path) && !"ACTIVE".equals(auth.getStatus())) { ApiError.write(response, 403, "NOT_APPROVED", "승인 대기 중인 계정입니다."); return; } request.setAttribute("memberNo", memberNo); // 컨트롤러와 쿼리가 이 값을 쓴다 chain.doFilter(req, res); } private String resolveBearer(String header) { if (header == null || !header.startsWith("Bearer ")) return null; return header.substring(7); } } URL 규칙은 이렇게 나눴다. 경로 규칙 /api/auth/** 공개 /api/admin/** 관리자만 /api/members/me/additional-info, /api/members/me/agreements 로그인만. 승인 전 계정도 써야 하는 API 그 외 /api/** 로그인하고 승인된(ACTIVE) 계정만 기본은 차단이다. 열어둘 것만 적고 나머지는 승인된 계정만 통과한다. 이것이 첫 겹이다. 두 번째 겹은 영구 삭제 같은 민감한 기능의 service 안에서 권한을 한 번 더 확인하는 것이고, 세 번째 겹은 수정과 삭제 쿼리의 WHERE에 로그인한 회원 번호를 넣는 것이다. 권한 필터를 통과한 사람이 남의 회원 번호를 요청 본문에 넣어 보내도 자기 행만 바뀐다. <update id="updateMyInfo"> UPDATE member SET manager_name = #{managerName}, manager_phone = #{managerPhone}, updated_at = now() WHERE member_no = #{memberNo} <!-- 필터가 넣어준 로그인 회원 번호. 요청 본문의 값이 아니다 --> </update> 버튼을 숨기는 것은 화면을 정리하는 일이고, 막는 일은 이 세 겹이 한다. 1-5. 외부 API를 부른 쪽: 네이버, 카카오, 메일 만든 API만 있는 것이 아니다. 부른 API가 셋 있다. 네이버 로그인, 카카오 로그인, 사내 SMTP 메일이다. 이 중 네이버가 가장 손이 많이 갔다. 네이버는 SDK를 걷어내고 인가 URL을 직접 조립했다. 같은 PC에서 로그아웃한 뒤 다음 사람이 네이버 버튼을 누르면 이전 사람 계정으로 바로 들어가지는 문제가 있었고, 원인은 네이버 쪽 로그인 쿠키가 남아 있어서였다. 매번 재인증을 강제하는 auth_type=reauthenticate 값을 붙여야 했는데 네이버 SDK에는 그 값을 받는 옵션이 없었고, 주소는 SDK가 내부에서 만들어서 끼워 넣을 수도 없었다. 그래서 문서를 보고 URL을 직접 만들었다. // 네이버 로그인 버튼. SDK 없이 인가 URL을 문서대로 조립한다. function openNaverLogin() { var state = crypto.getRandomValues(new Uint32Array(1))[0].toString(16); sessionStorage.setItem('naverState', state); var url = 'https://nid.naver.com/oauth2.0/authorize' + '?response_type=token' // 토큰이 콜백 URL의 해시(#)에 실려 온다
1. ACL ACL(Access Control List)은 네트워크에서 어떤 트래픽을 통과시킬지 정하는 규칙이다. Inbound : 라우터 안으로 들어오는 트래픽 Outbound : 라우터 밖으로 나가는 트래픽 규칙은 위에서부터 순서대로 확인한다. 마지막에는 기본적으로 deny any 가 있어서 허용할 건 따로 permit 을 해줘야 한다. Standard ACL 출발지 IP만 확인하는 ACL이다. access-list 1 permit 192.168.10.0 0.0.0.255 Extended ACL 출발지 IP뿐만 아니라 목적지 IP, 프로토콜, 포트 번호까지 확인할 수 있다. 그래서 Standard ACL보다 더 자세하게 설정할 수 있다. 2. Wildcard Mask ACL에서는 Subnet Mask 대신 Wildcard Mask를 사용한다. 예를 들면, 255.255.255.0 → 0.0.0.255 여기서 0 은 비교하고, 1 은 무시한다는 뜻이다. 3. NAT NAT(Network Address Translation)는 내부에서 사용하는 IP를 외부에서 사용할 수 있는 IP로 바꿔주는 기술이다. 내부 PC가 사설 IP를 사용하고 있어도 NAT를 거치면 외부 네트워크와 통신할 수 있다. Static NAT Static NAT는 내부 IP와 외부 IP를 1:1로 고정해서 연결하는 방식이다. ip nat inside source static 192.168.10.2 203.0.113.3 그리고 내부와 외부 인터페이스도 정해줘야 한다. interface g0/0 ip nat inside interface s0/3/0 ip nat outside Dynamic NAT Dynamic NAT는 외부 IP를 여러 개 준비해 두고, 필요할 때 하나씩 할당하는 방식이다. access-list 1 permit 192.168.10.0 0.0.0.255 ip nat pool NATPOOL 203.0.113.3 203.0.113.5 netmask 255.255.255.0 ip nat inside source list 1 pool NATPOOL Static NAT처럼 미리 하나씩 고정하는 게 아니라, Pool 안에서 사용 가능한 IP를 골라서 사용한다. PAT PAT는 외부 IP 하나를 여러 내부 장치가 같이 사용하는 방식이다. 같은 IP를 같이 쓰기 때문에 포트 번호를 이용해서 각각의 통신을 구분한다. access-list 1 permit 192.168.10.0 0.0.0.255 ip nat inside source list 1 interface s0/3/0 overload 4. NAT 종류 정리 종류 특징 Static NAT 내부 IP와 외부 IP를 1:1로 고정 Dynamic NAT IP Pool에서 사용 가능한 주소를 할당 PAT 외부 IP 하나를 여러 장치가 같이 사용 5. 확인 명령어 NAT 설정이 제대로 됐는지 확인할 때는 아래 명령어를 사용한다. show ip nat translations show ip nat statistics
요즘 AI를 공부하면서 가장 어려운 것은 기술 자체보다도 무엇을 공부해야 하는지 결정하는 것 인 것 같다. 하루가 멀다 하고 새로운 모델이 나오고, Claude, GPT, Gemini, Qwen 같은 모델뿐만 아니라 Agent, MCP, RAG, Computer Use, Local LLM, Multimodal, Vibe Coding 같은 새로운 키워드가 계속 등장한다. 처음에는 새로운 모델이 나오면 설치해보고, 새로운 AI 서비스가 나오면 사용해보는 것이 AI 트렌드를 따라가는 것이라고 생각했다. 그런데 최근 직접 여러 가지를 만들어보면서 생각이 조금 달라졌다. 중요한 것은 새로운 AI를 얼마나 많이 알고 있느냐가 아니라, AI를 실제 시스템에 어떻게 연결할 수 있느냐는 것 아닐까? 그래서 앞으로 어떤 방향으로 AI를 공부할지 한번 정리해보려고 한다. 나는 지금 어느 정도일까? 나는 토목공학과 컴퓨터공학을 함께 공부했고 현재는 웹 개발을 하면서 스마트건설 관제 시스템을 개발하고 있다. AI와 관련해서도 조금씩 여러 가지를 시도해봤다. Claude API를 이용해 수학 문제를 분석하고 풀이와 채점을 생성하는 시스템을 만들어보기도 했고, Mathpix OCR과 LLM을 연결해서 이미지에서 수식을 추출하고 문제 데이터를 만드는 파이프라인도 구현해봤다. 최근에는 Ollama와 Qwen을 이용해서 Local LLM 환경을 구축하고, OpenClaw를 연결해 AI가 브라우저를 직접 조작하도록 테스트하고 있다. 실제로 사용자 명령 ↓ OpenClaw ↓ Local Qwen ↓ Browser ↓ Velog 게시글 작성 같은 자동화도 실험하고 있다. 여기까지 해보면서 느낀 것은 내가 아직 AI 모델 자체를 연구하는 사람은 아니지만, AI를 기존 서비스와 연결해서 실제 기능으로 만드는 쪽에 점점 가까워지고 있다는 것 이다. 그리고 앞으로도 이 방향으로 공부하는 것이 나에게 가장 잘 맞는다고 생각한다. AI 공부도 단계가 있다고 생각한다 개인적으로 AI 활용 수준을 대략 이렇게 나눠볼 수 있을 것 같다. 단계 AI 활용 방식 1. 소비자 ChatGPT, Claude 등을 사용한다 2. 활용자 코딩, 문서 작성, 분석 등 업무에 적극 사용한다 3. 적용자 API를 이용해 서비스에 AI 기능을 붙인다 4. 설계자 Agent와 Tool을 조합해 AI가 일을 수행하도록 만든다 5. 운영자 성능, 비용, 평가, 보안까지 관리한다 지금의 나는 개인적으로 3단계 후반에서 4단계로 넘어가는 과정 이라고 생각한다. 단순히 ChatGPT를 잘 사용하는 것을 넘어 API와 Local LLM을 사용해봤고, 이제는 Agent가 여러 도구를 사용해서 실제 작업을 수행하도록 만드는 것에 관심을 가지고 있기 때문이다. 그래서 지금부터의 공부 방향도 여기에 맞춰야 한다고 생각한다. 1. 가장 먼저 공부할 것: Agent 내가 가장 먼저 제대로 공부하려는 분야는 AI Agent 다. 기존 LLM 서비스는 기본적으로 이런 구조였다. 사용자 ↓ LLM ↓ 답변 하지만 최근 AI 시스템은 점점 이런 방향으로 발전하고 있다. 사용자 목표 ↓ Agent ↓ 상황 판단 ↓ 필요한 Tool 선택 ↓ Tool 실행 ↓ 결과 확인 ↓ 다음 행동 결정 ↓ 작업 완료 OpenAI 역시 2026년 9월 Agents API를 공개하면서 장시간 작업, Tool 사용, Context 관리, Subagent 협업 등을 Agent 시스템의 핵심 요소로 두고 있다. Agents SDK 역시 Agent loop, handoff, tool integration, state, observability 등을 애플리케이션에서 직접 제어하는 구조로 제공되고 있다. 즉 이제는 “좋은 답변을 생성하는 AI” 뿐만 아니라 “목표를 주면 필요한 작업을 수행하는 AI” 를 공부해야 한다. 내가 공부해야 할 핵심도 프레임워크 사용법보다 Agent의 기본 구조라고 생각한다. Prompt ↓ Reasoning ↓ Tool Selection ↓ Tool Calling ↓ Execution ↓ Observation ↓ Next Action 여기에 Context 관리, Memory, 승인 과정, 실패 처리, 재시도 같은 개념들이 붙는다. 2. Tool Calling을 제대로 이해하기 Agent를 공부하려면 결국 Tool Calling 을 이해해야 한다. 예를 들어 AI에게 현재 현장에서 위험한 구역을 찾아줘. 라고 요청했다고 생각해보자. AI가 직접 센서 데이터를 알고 있는 것은 아니다. 대신 이런 Tool을 사용할 수 있다. get_sensor_data() get_cctv_events() get_worker_location() get_weather() get_equipment_status() Agent는 질문을 분석하고 필요한 Tool을 결정한다. 현재 위험한 구역을 찾아줘 ↓ 센서 데이터 필요 ↓ get_sensor_data() ↓ CCTV 이벤트 필요 ↓ get_cctv_events() ↓ 작업자 위치 필요 ↓ get_worker_location() ↓ 결과 종합 이 구조를 제대로 이해하면 LangChain이나 CrewAI 같은 특정 프레임워크를 몰라도 Agent가 어떻게 동작하는지 이해할 수 있다. 그래서 앞으로는 라이브러리 사용법보다 먼저 Tool Schema, Structured Output, Agent Loop, Context 관리 부터 공부하려고 한다. 3. MCP 다음으로 공부하려는 것은 MCP(Model Context Protocol)다. MCP는 2024년 Anthropic이 공개한, AI 애플리케이션과 외부 데이터 및 도구를 연결하기 위한 오픈 프로토콜이다. 처음에는 Claude 중심의 기술처럼 보였지만 이후 생태계가 크게 확장되었고, 현재는 Agent가 외부 시스템과 연결되는 중요한 인터페이스 중 하나로 발전하고 있다. MCP를 이해하기 쉽게 표현하면 AI ↓ MCP ↓ 외부 시스템 이다. 예를 들어 스마트건설 관제 시스템용 MCP Server를 만든다고 생각해보자. construction-mcp getSensorData getCctvEvents getWorkerLocation getWeather getEquipmentStatus getAlarmHistory 이렇게 만들어두면 MCP를 지원하는 Agent가 이 기능들을 사용할 수 있다. 내 입장에서는 이것이 꽤 재미있는 주제다. 현재 개발하고 있는 스마트건설 관제 시스템 자체가 센서, CCTV, 작업자, 장비, 기상 데이터처럼 다양한 데이터를 가지고 있기 때문이다. 기존에는 사람이 대시보드를 열어 데이터를 하나씩 확인했다면, 앞으로는 Agent에게 지금 현장에서 확인해야 할 위험 요소 알려줘. 라고 요청하는 방식으로 바뀔 수도 있다. 그리고 MCP도 계속 발전하고 있다. 2026년 7월 공개된 MCP 규격에서는 stateless 구조, 장시간 작업을 위한 Tasks 확장, MCP Apps, 인증 구조 개선 등이 포함됐다. 단순히 로컬 Tool 몇 개를 연결하는 규격에서 실제 Agent 인프라에 가까운 방향으로 발전하고 있다는 점이 흥미롭다. 4. RAG는 여전히 중요하다 Agent가 행동을 담당한다면 RAG는 지식을 가져오는 역할 을 담당한다고 볼 수 있다. 단순하게 생각하면 질문 ↓ 관련 문서 검색 ↓ 필요한 내용 추출 ↓ LLM에게 전달 ↓ 답변 구조다. 하지만 실제로 사용하려면 단순히 PDF → Embedding → Vector DB → GPT 정도에서 끝나서는 안 된다고 생각한다. 앞으로 공부하면서 Document ↓ Chunking ↓ Embedding ↓ Vector Search ↓ Keyword Search ↓ Hybrid Search ↓ Reranking ↓ Context ↓ LLM 과정을 직접 만들어보고 싶다. 특히 스마트건설과 결합하면 활용할 데이터가 많다. 예를 들어 안전관리 매뉴얼, 시공 지침, 장비 매뉴얼, 센서 설명서, 현장 보고서 같은 문서를 검색하도록 만들 수 있다. 그러면 Agent에게 현재 CO 센서 농도가 평소보다 높아졌는데 어떤 조치를 해야 하지? 라고 질문했을 때 실시간 Sensor Data + 안전관리 문서 RAG ↓ Agent 구조로 답변을 만들 수 있다. 이때 답만 생성하는 것이 아니라 어떤 센서 데이터와 어떤 문서를 근거로 판단했는지 함께 보여주는 것 까지 구현해보고 싶다. 5. Eval을 공부해야 하는 이유 최근 AI 개발을 해보면서 가장 부족하다고 느끼는 부분이 이것이다. 보통 기능을 만들고 나면 몇 번 테스트해봤는데 잘 되네. 하고 끝내기 쉽다. 하지만 실제 서비스라면 그것으로 부족하다. 예를 들어 Agent에게 100개의 테스트 상황을 주고 올바른 Tool을 선택했는가? 필요한 Tool을 빠뜨리지 않았는가? 잘못된 정보를 만들지는 않았는가? RAG에서 올바른 문서를 검색했는가? 응답시간은 얼마나 걸렸는가? 비용은 얼마나 발생했는가? 를 측정해야 한다. OpenAI의 현재 Agent 개발 문서에서도 tracing과 observability, evaluation을 Agent workflow 개선 과정의 중요한 요소로 다루고 있다. 결국 AI 시스템을 만드는 것과 신뢰할 수 있는 AI 시스템을 만드는 것 은 다른 문제다. 앞으로는 AI 기능 하나를 만들더라도 테스트 Dataset을 같이 만들어보려고 한다. 6. Multimodal AI 내가 특히 관심을 가지는 부분이다. 스마트건설에서는 텍스트보다 이미지와 영상 데이터가 굉장히 많다. 현재도 CCTV 영상과 실시간 스트리밍 데이터를 다루고 있기 때문에 Vision AI와 연결하기 좋은 환경이다. 예를 들어 이런 구조를 만들 수 있다. CCTV ↓ Vision Model ↓ 작업자 / 장비 / 위험상황 분석 ↓ ┌─ Sensor Data │ ├─ Worker Data │ ├─ Weather Data │ ▼ Agent ↓ 위험 상황 판단 여기까지 가면 AI는 단순히 챗봇이 아니다. 실제 현장의 다양한 데이터를 종합하는 인터페이스 가 된다. 토목공학과 컴퓨터공학을 같이 공부한 내 배경도 이 부분에서 꽤 재미있게 활용할 수 있을 것 같다. 7. Local LLM도 계속 공부한다 최근 Ollama와 Qwen을 사용하면서 Local LLM도 계속 테스트하고 있다. 처음에는 단순히 ollama run qwen 정도로 모델을 실행하는 것이 신기했다. 하지만 앞으로는 실행 자체보다 어떻게 효율적으로 모델을 운영할 것인가 를 공부해야 한다고 생각한다. 특히 Model Size Quantization VRAM Context Length Tokens/sec Embedding GPU Inference vLLM GGUF 같은 개념을 더 공부하고 싶다. vLLM 같은 추론 서버는 현재 다양한 Quantization 방식과 OpenAI 호환 API 기반 서빙 등을 지원하기 때문에 Local AI를 서비스 관점에서 공부할 때 좋은 실습 대상이라고 생각한다. Quantization은 모델의 메모리 사용량을 줄여 더 다양한 하드웨어에서 모델을 실행할 수 있도록 하는 대표적인 최적화 방법이다. 특히 건설현장에서는 인터넷 연결이나 데이터 보안 문제도 발생할 수 있기 때문에 Local AI와 Cloud AI를 조합하는 구조도 생각해볼 수 있다. 일반적인 AI 작업 ↓ Cloud LLM 민감한 현장 데이터 ↓ Local LLM 복잡한 판단 ↓ Cloud / Local 선택 이런 Hybrid AI Architecture도 한번 직접 구현해보고 싶다. 그런데 모든 AI 프레임워크를 공부할 필요는 없다 AI를 공부하다 보면 끝이 없다. LangChain이 보이면 LangChain을 공부하고, LangGraph가 나오면 LangGraph를 공부하고, CrewAI가 나오면 CrewAI를 공부하고, n8n이 유행하면 n8n을 공부하고, OpenClaw가 나오면 OpenClaw를 공부하게 된다. 그런데 이렇게 공부하면 계속 새로운 도구만 따라가게 된다. 실제로 AI 개발 도구 자체의 변화 속도도 빠르다. 예를 들어 OpenAI는 2026년 Agent Builder와 기존 Evals 플랫폼의 종료 계획을 발표했고, Agent 개발은 Agents SDK 등의 코드 기반 도구로 이동시키고 있다. 그래서 특정 프레임워크를 외우기보다 Tool Calling Agent Loop Context MCP Retrieval Evaluation Observability Local Inference 같은 변하지 않는 구조를 먼저 이해하는 것이 더 중요하다 고 생각한다. 프레임워크는 그다음이다. 그래서 나는 무엇을 만들 것인가? 공부를 위해 새로운 토이 프로젝트를 계속 만드는 것보다 지금까지 해왔던 것들을 하나로 연결해보려고 한다. 가칭 Smart Construction AI Copilot 이다. 전체 구조는 대략 이렇게 생각하고 있다. User │ ▼ Next.js Dashboard │ ▼ AI Copilot │ ▼ Agent ┌─────────────┼─────────────┐ │ │ │ Sensor Tool CCTV Tool Weather Tool │ │ │ Worker Tool Equipment Tool Document RAG │ │ │ └─────────────┼─────────────┘ │ ▼ MCP Server │ ┌─────────┴─────────┐ │ │ PostgreSQL API 사용자는 단순하게 질문한다. 현재 현장에서 가장 위험한 구역을 알려줘. 그러면 Agent가 필요에 따라 센서 데이터 확인 ↓ CCTV 이벤트 확인 ↓ 작업자 위치 확인 ↓ 기상 데이터 확인 ↓ 안전관리 문서 검색 ↓ 위험 요소 분석 ↓ 근거와 함께 결과 제공 을 수행한다. 예를 들면 결과가 이렇게 나올 수도 있다. A터널 3구간 주의 - CO 농도 지속 상승 - 현재 작업자 4명 위치 - 최근 10분간 센서 값 증가 - CCTV에서 작업 진행 확인 권장 조치: 작업자에게 현장 상황 확인 요청 근거: Sensor #CO-03 CCTV #TUNNEL-02 안전관리 매뉴얼 3.2절 물론 실제 안전 판단을 AI에게 전적으로 맡길 수는 없다. 그래서 이런 시스템에서 더욱 중요한 것이 근거 데이터, 불확실성 표시, 사람의 최종 확인, Tool 권한 관리 라고 생각한다. 이 부분까지 포함해야 실제 사용 가능한 AI 시스템에 가까워진다. 앞으로의 공부 순서 현재 내 기준으로는 이렇게 공부하려고 한다. Tool Calling → Agent → MCP → RAG → Eval & Observability → Multimodal → Local AI → 실제 서비스 통합 그리고 각 개념을 따로 공부하는 것에서 끝내지 않고 Smart Construction AI Copilot이라는 하나의 프로젝트에 계속 붙여볼 생각이다. Agent를 공부하면 Sensor Tool을 만든다. MCP를 공부하면 Construction MCP Server를 만든다. RAG를 공부하면 안전관리 문서를 연결한다. Multimodal을 공부하면 CCTV를 연결한다. Local LLM을 공부하면 Qwen을 붙인다. Eval을 공부하면 위험 상황 테스트 Dataset을 만든다. 이런 방식이면 공부한 결과가 그대로 하나의 시스템에 쌓인다. 마무리 AI 트렌드를 따라간다는 것은 모든 신제품의 이름을 알고 있는 것이 아닐지도 모른다. GPT의 다음 버전이 무엇인지, Claude의 최신 모델이 무엇인지, 어떤 Agent 프레임워크가 GitHub Star를 가장 많이 받았는지를 빠르게 아는 것도 물론 재미있다. 하지만 개발자로서 더 중요한 것은 결국 AI가 어떤 방향으로 발전하고 있고, 그 기술을 내가 만드는 시스템에 어떻게 적용할 것인가 라고 생각한다. 지금까지는 AI에게 "이거 해줘" 라고 질문하는 방법을 배웠다면, 앞으로는 "이 목표를 달성해줘" 라고 요청했을 때 AI가 필요한 정보를 찾고, 도구를 선택하고, 실행하고, 결과를 검증하는 시스템을 공부해보고 싶다. 그래서 앞으로 당분간 나의 AI 공부 키워드는 Agent, Tool Calling, MCP, RAG, Eval, Multimodal, Local AI 가 될 것 같다. 그리고 최종 목표는 단순히 AI 기술을 많이 아는 개발자가 아니라, 토목 도메인과 웹 개발, 실시간 관제 데이터, AI를 하나의 시스템 안에서 연결할 수 있는 개발자 가 되는 것이다. 이 블로그에도 앞으로 하나씩 직접 구현하면서 그 과정을 기록해보려고 한다.
이번에는 ROS 2에서 노드들이 서로 데이터를 주고받는 메시지 통신 방식에 대해 정리했다. ROS 2에서는 여러 개의 노드가 서로 유기적으로 연결되어 하나의 시스템을 구성한다. 각각의 노드는 독립적인 프로그램처럼 동작하지만, 노드 사이에서 메시지를 주고받으면서 서로 연동된다. 수행해야 하는 작업이 많아질수록 노드의 수와 노드 사이의 메시지 연결도 증가하며, 이를 통해 ROS 시스템을 확장할 수 있다. 1. Topic Topic은 ROS에서 가장 기본적으로 사용되는 메시지 통신 방식이다. 토픽은 비동기식 단방향 메시지 송수신 방식으로, 메시지를 발행하는 Publisher 와 메시지를 구독하는 Subscriber 사이에서 통신이 이루어진다. Publisher │ │ Message ↓ Topic │ ↓ Subscriber 토픽 통신은 다음과 같이 다양한 형태로 구성할 수 있다. 1 : N N : 1 N : N 즉, 하나의 Publisher가 여러 Subscriber에게 메시지를 전달할 수도 있고, 여러 Publisher와 Subscriber가 연결되는 구조도 가능하다. 토픽은 비동기적으로 연속적인 데이터를 전달할 수 있기 때문에 ROS 메시지 통신에서 가장 널리 사용되는 방식이다. 2. Service Service는 토픽과 달리 동기식 양방향 메시지 송수신 방식이다. 서비스를 요청하는 쪽을 Service Client , 요청을 받아 처리하고 결과를 전달하는 쪽을 Service Server 라고 한다. Service Client │ │ Request ↓ Service Server │ │ Response ↓ Service Client 즉, 특정 작업을 요청하고 그 작업이 수행된 후 결과를 받는 방식이다. 서비스에서 사용하는 요청과 응답 메시지는 srv 메시지 형태로 구성된다. 3. Action Action은 서비스와 토픽의 특성을 함께 사용하는 통신 방식이다. 액션에서는 Action Client 가 수행하고자 하는 목표인 Goal을 전달하고, Action Server 는 해당 목표를 수행하면서 중간 상태를 Feedback으로 전달하며 작업이 끝나면 최종 결과인 Result를 전달한다. Action Client │ │ Goal ↓ Action Server │ ├──── Feedback ───→ │ └──── Result ─────→ 따라서 액션은 비동기식과 동기식 양방향 통신의 특성을 함께 가진다. Action의 구조 액션의 동작 방식을 조금 더 자세히 보면 토픽과 서비스가 혼합된 형태로 볼 수 있다. Goal → 서비스와 같은 방식 Result → 서비스와 같은 방식 Feedback → 토픽과 같은 방식 즉, Action ├── Goal ├── Feedback └── Result 의 구조로 데이터를 주고받는다. 액션에서 사용하는 메시지도 action 메시지 형태로 구분된다. 4. Parameter Parameter는 노드의 설정값이나 매개변수를 외부에서 쉽게 지정하거나 변경할 수 있도록 사용하는 기능이다. 각 노드는 자신의 파라미터를 가지고 있으며, 외부에서 파라미터 값을 설정하거나 변경하고 가져올 수 있다. Parameter Client │ │ Set / Get ↓ Parameter Server │ ↓ Node 파라미터는 서비스와 비슷하게 값을 요청하고 변경할 수 있지만, 목적에는 차이가 있다. 서비스는 특정 작업에 대한 요청과 응답을 위한 통신이고, 파라미터는 노드 내부 또는 글로벌 설정값을 지정하거나 변경하고 가져오기 위한 목적으로 사용된다. 5. ROS 2 메시지 통신 정리 이번 내용을 정리하면 ROS 2에서는 목적에 따라 다양한 메시지 통신 방식을 사용할 수 있다. 통신 방식 특징 주요 용도 Topic 비동기식 단방향 연속적인 데이터 전달 Service 동기식 양방향 요청 및 응답 Action 비동기식 + 동기식 양방향 Goal, Feedback, Result가 필요한 작업 Parameter 설정값의 Set / Get 노드의 매개변수 설정 및 변경 전체적으로 보면 ROS 2의 노드는 서로 독립적으로 동작하면서도 이러한 메시지 통신 방식을 통해 서로 연결된다. ROS 2 Node │ ┌─────────────┼─────────────┐ ↓ ↓ ↓ Topic Service Action │ │ │ Publisher / Request / Goal / Feedback Subscriber Response / Result │ ↓ Parameter Set / Get
Trong hoạt động kinh doanh bán lẻ, bên cạnh bảng hiệu chính ở mặt tiền, việc trang bị thêm một chiếc bảng hiệu phụ đặt ngay trên vỉa hè hoặc lối đi là chiến lược hiệu quả để thu hút sự chú ý của người đi đường. Trong đó, giải pháp sử dụng biển quảng cáo đứng đang được các quán cafe, tiệm trà sữa, spa, nhà hàng và cửa hàng thời trang đặc biệt ưa chuộng nhờ tính linh hoạt và khả năng tiếp cận thị giác ở tầm thấp. Để giúp quý khách hàng dễ dàng cân đối ngân sách và lựa chọn vật liệu phù hợp, Quảng Cáo Tùng Huyền cập nhật chi tiết dịch vụ và báo giá làm biển quảng cáo đứng trọn gói, minh bạch trên hệ thống website chính thức. Ưu Điểm Nổi Bật Của Biển Quảng Cáo Đứng Không phải ngẫu nhiên mà loại bảng hiệu này xuất hiện phổ biến trên khắp các tuyến phố kinh doanh. Sở hữu một thiết kế biển quảng cáo đứng chuẩn mực đem lại nhiều lợi ích thiết thực: Tối ưu tầm nhìn: Đặt ở vị trí vừa tầm mắt của người tham gia giao thông, giúp thu hút ánh nhìn nhanh chóng ngay cả khi người đi đường di chuyển ở tốc độ cao. Tính linh hoạt cao: Nhờ kích thước gọn nhẹ và khung chân chắc chắn, bạn có thể dễ dàng di chuyển, cất giữ vào bên trong cửa hàng khi hết giờ làm việc hoặc điều chỉnh vị trí theo nhu cầu. Tiết kiệm chi phí đầu tư: So với các loại Pano hay bảng hiệu mặt tiền khổ lớn, chi phí sản xuất bảng hiệu đứng tương đối mềm nhưng vẫn đem lại hiệu quả truyền thông cao. Đa dạng về mẫu mã: Dễ dàng tùy biến theo nhiều phong cách từ hiện đại, sang trọng đến vintage, độc đáo. Các Loại Biển Quảng Cáo Đứng Phổ Biến Hiện Nay Tùy thuộc vào ngành nghề kinh doanh và ngân sách đầu tư, khách hàng có thể lựa chọn nhiều loại biển quảng cáo đứng khác nhau tại Quảng Cáo Tùng Huyền: Biển Đứng Khung Sắt Căng Bạt Hiflex / In Decal Đây là dòng sản phẩm có mức giá tiết kiệm nhất. Khung xương bằng sắt hộp mạ kẽm chống gỉ, bề mặt căng bạt Hiflex in kỹ thuật số sắc nét hoặc dán Decal cao cấp. Loại biển này phù hợp cho các quán ăn bình dân, tiệm sửa xe, đại lý bán lẻ. Biển Đứng Hộp Đèn Mica Hút Nổi Đối với các mô hình kinh doanh vào ban đêm như quán cafe, tiệm trà sữa, quán pub hay spa, biển hộp đèn mica đứng gắn hệ thống LED bên trong là lựa chọn lý tưởng. Ánh sáng phát ra đều, hình ảnh rực rỡ giúp cửa hàng luôn nổi bật từ xa. Biển Đứng Khung Gỗ / Khung Sắt Nghệ Thuật Dạng Standee Phù hợp với các không gian mang phong cách vintage, boutique hoặc tiệm bakery. Mặt bảng có thể làm bằng gỗ, mica hoặc biển huỳnh quang vẽ tay linh hoạt thay đổi menu hàng ngày. Biển Đứng Đèn LED Điệu Tử / Màn Hình LED Loại biển hiện đại cho phép hiển thị chữ chạy, hình ảnh hoặc video khuyến mãi nhấp nháy bắt mắt, thu hút khách hàng cực kỳ hiệu quả trong điều kiện thiếu sáng. Tham Khảo Báo Giá Làm Biển Quảng Cáo Đứng Tại Bangquangcaoth.com Chi phí sản xuất bảng hiệu phụ thuộc vào nhiều yếu tố như kích thước, chất liệu khung, chất liệu in và hệ thống chiếu sáng đi kèm. Để giúp khách hàng không gặp phải tình trạng phát sinh chi phí ngoài dự kiến, Quảng Cáo Tùng Huyền luôn công khai minh bạch bảng giá từng hạng mục. Quý khách hàng có thể tham khảo chi tiết bảng giá các vật liệu và mẫu thiết kế biển quảng cáo đứng mới nhất tại quảng cáo Tùng Huyền Lý Do Nên Lựa Chọn Quảng Cáo Tùng Huyền Là đơn vị thi công bảng hiệu uy tín với nhiều năm kinh nghiệm, Quảng Cáo Tùng Huyền cam kết đem đến sản phẩm biển quảng cáo đứng chất lượng nhất: Tư vấn & Thiết kế miễn phí: Hỗ trợ lên bản vẽ thiết kế 2D/3D phù hợp với bộ nhận diện thương hiệu của cửa hàng. Sản xuất trực tiếp tại xưởng: Trang bị máy cắt Laser, CNC, máy in khổ lớn hiện đại, đảm bảo sản phẩm hoàn thiện sắc nét, cứng cáp. Chất liệu bền bỉ: Khung chân sắt gia công chắc chắn, chống chịu gió gập, chân biển có chân đế gia tải chống lật an toàn. Giao hàng & Lắp đặt nhanh chóng: Đảm bảo đúng tiến độ cam kết, chính sách bảo hành dài hạn và hỗ trợ kỹ thuật tận tâm. Nếu bạn đang muốn đầu tư một mẫu biển quảng cáo đứng thẩm mỹ, thu hút khách hàng với mức giá ưu đãi nhất, hãy liên hệ ngay với Quảng Cáo Tùng Huyền hoặc truy cập website để nhận tư vấn trực tiếp! Thông tin liên hệ Quảng Cáo Tùng Huyền: Địa chỉ: 204 F325, Phường Đồng Thuận, Quảng Trị Hotline/Zalo: 0877 912 313 Email: inantunghuyen@gmail.com Website: https://bangquangcaoth.com/
이번 교안에서는 LangGraph 실행을 사람의 검토 지점에서 멈추고, 같은 작업을 재개하는 HITL 흐름과 생성 과정·진행 상태를 스트리밍으로 확인하는 방법을 정리했다. HITL로 사람의 판단을 실행 흐름에 넣기 HITL(Human-in-the-loop)은 자동 실행 중간에 사람의 판단을 받아 다음 작업을 정하는 방식이다. 서비스 점검 안내문 그래프는 초안을 만든 다음 사람이 승인·수정 요청·반려 중 하나를 선택한다. START → 초안 작성 → 사람 검토 ├─ 승인 → 안내문 확정 → END ├─ 수정 → 수정 후 재검토 └─ 반려 → END State에는 확정된 점검 정보, 현재 초안, 사람의 결정과 수정 의견, 최종 확정문을 둔다. 초안 작성과 수정은 LLM 노드이고, 검토 노드는 interrupt() 로 사람의 응답을 기다린다. interrupt로 멈추고 Command로 재개하기 interrupt() 는 검토할 초안과 선택지 같은 데이터를 호출자에게 전달하고 그래프를 멈춘다. 대기 상태를 저장하고 나중에 찾을 수 있도록 체크포인터와 thread_id 가 필요하다. 사람의 응답을 전달할 때는 대기 중인 실행과 같은 thread_id 를 사용한다. 한 요청만 대기 중이라면 Command(resume=응답) 으로 재개할 수 있다. 중단 ID를 알고 있거나 여러 요청을 구분해야 한다면 현재 요청의 ID를 키로 지정한다. interrupt_id = graph.get_state(config).interrupts[0].id result = graph.invoke( Command(resume={interrupt_id: { "action": "edit", "feedback": "기존 예약 조회가 가능하다는 점을 먼저 알려 주세요.", }}), config, ) 중단 요청의 stage 는 화면에서 어떤 검토 단계인지 표시하기 위한 값이다. 중단 ID는 응답할 요청을 지정하고, thread_id 는 전체 작업 기록을 구분한다. stage 가 사용자 권한을 확인해 주는 것은 아니므로 실제 서비스의 권한 검사는 별도로 구현해야 한다. 재개하면 대기 중이던 노드가 처음부터 다시 실행되고, 같은 interrupt() 에 도달했을 때 전달한 응답을 반환한다. 따라서 그 노드에서 interrupt() 앞에 놓인 코드는 다시 실행될 수 있다. 중복 실행되면 안 되는 외부 발송이나 결제는 대기 노드 앞에 두지 말고 승인 이후에 실행하며, 재시도에도 중복되지 않도록 설계해야 한다. 수정 요청은 LLM이 의견을 반영해 새 초안을 만들고, 다시 사람의 검토를 기다린다. 사람이 직접 고친 문장을 edited_text 로 전달해 승인하는 경로도 만들 수 있다. 이 경우 수정 모델 호출 없이 사람이 편집한 문장을 확정한다. 승인과 확정은 별도 작업으로 둘 수 있다 교안의 확장 실습은 채용공고의 내용 검토 와 게시 승인 을 두 단계로 나눈다. 공고 작성 → 내용 검토 ── 수정 → 재검토 ├─ 승인 → 게시 승인 └─ 반려 → END ├─ 승인 → 확정 └─ 반려 → END 내용이 정확하다는 승인과 실제 게시를 허가하는 승인은 서로 다른 판단이다. 두 번째 승인까지 끝난 뒤에만 approved_post 를 채운다. 첫 검토의 결정과 게시 결정은 별도 State 필드에 저장한다. 여러 HITL 단계가 한 작업에 있으면 매번 get_state(config).interrupts 에서 현재 대기 중인 요청의 ID 를 가져와 응답해야 한다. 수정 후 다시 대기한 요청은 같은 stage 를 쓰더라도 새로운 interrupt ID를 가질 수 있다. 검토를 진행하는 동안에는 같은 thread_id 를 유지한다. 스트리밍으로 실행 상황을 보여 주기 스트리밍은 그래프가 실행되는 동안 결과를 차례로 받아 화면에 보여 주는 방식이다. 교안에서는 안내문 생성 문장과 “작성 중”, “사람 검토 대기” 같은 진행 상태를 별도 화면 영역에 표시한다. 모드 받는 데이터 주 용도 updates 각 노드가 갱신한 필드 어떤 노드가 무엇을 바꿨는지 보기 values 단계별 전체 State 현재 초안과 결정 상태 보기 messages 모델 출력 조각과 메타데이터 작성 중인 문장을 이어서 표시 custom get_stream_writer() 로 보낸 값 사용자 정의 진행 알림 표시 messages 는 생성되는 토큰을 받을 수 있어 State에 messages 필드가 없어도 쓸 수 있다. custom 진행 정보는 화면 표시용이며 State를 갱신하지 않는다. 네 가지 모드는 한 번의 stream() 실행에 함께 지정할 수 있고, display_id 와 .update() 를 사용하면 진행·State·본문을 화면의 서로 다른 자리에 갱신할 수 있다. stream() 도 그래프를 실행한다. 새 입력으로 시작하면 LLM 호출이 실제로 발생하므로, 이미 끝난 실행을 수동으로 다시 스트리밍하는 조회 기능처럼 생각하면 안 된다. 실행이 끝나거나 interrupt에서 멈춘 뒤 최종 State를 읽는 것은 이미 저장된 결과를 확인하는 일이다. v2 StreamPart와 v3 실행 객체 교안은 두 출력 방식을 비교한다. stream(..., version="v2") 는 {type, data, ns} 형태의 항목을 내보낸다. type 을 확인해 messages , updates , values , custom 등의 data 를 처리한다. stream_events(..., version="v3") 는 실행 객체를 반환한다. run.messages , run.values , run.output , run.interrupts 같은 속성으로 필요한 정보를 읽는다. v2는 각 항목의 종류를 직접 분기해 처리하고, v3는 실행 객체의 속성으로 결과를 골라 읽는 방식이다. 교안은 v3를 실험 단계 API로 다루므로, 실제 사용 시 프로젝트에서 고정한 LangGraph 버전과 해당 버전의 문서를 확인해야 한다. 정리 interrupt() 는 사람의 검토를 위해 실행을 멈추고, Command(resume=...) 은 저장된 실행에 응답을 전달한다. 재개 시 대기 노드는 처음부터 실행되므로, 앞부분에 중복 실행 위험이 있는 작업을 두지 않는다. thread_id 는 작업 기록을, interrupt ID는 응답할 현재 요청을 구분한다. stage 는 화면 표시용이다. 수정 뒤에는 다시 사람의 검토로 돌아가며, 필요하면 내용 승인과 게시 승인을 따로 둘 수 있다. updates , values , messages , custom 은 각각 노드 변경, 전체 State, 생성 문장, 사용자 정의 진행 정보를 전달한다. stream() 은 관찰만 하는 기능이 아니라 그래프를 실행한다. 참고 자료 LangGraph interrupt와 재개 LangGraph 스트리밍 LangGraph event streaming
Multi-Agent 실습에서 Contract 를 배우고, 다음 실습에서 Supervisor 를 배웠다. 각각 배울 때는 이해가 됐는데 둘을 연결해서 생각하니 한 가지가 헷갈렸다. Contract도 Agent를 통제하고 Supervisor도 Agent를 통제한다면, 둘은 뭐가 다른 걸까? 실습 흐름을 다시 따라가보니 둘의 차이는 통제하는 대상과 시점 에 있었다. Contract는 Agent가 만든 결과를 검증한다 02_agent-role-and-contract 실습에서는 Agent가 결과를 생성한 뒤 Contract 검증을 거쳤다. 예를 들어 Budget Agent가 결과를 생성하면 Contract를 통해 정해진 형식과 조건을 만족하는지 확인하고, 검증된 결과만 다음 단계에서 사용할 수 있었다. Budget Agent ↓ 결과 생성 ↓ Contract 검증 ↓ 검증 성공 → 다음 단계에서 사용 검증 실패 → 다음 단계로 전달하지 않음 즉 Contract가 확인하는 것은 “이 Agent가 만든 결과를 사용해도 되는가?” 였다. Contract는 다음 Agent를 선택하는 것이 아니라, 이미 생성된 결과가 시스템에서 사용 가능한 결과인지 검증하는 역할 을 한다. Supervisor는 현재 State를 보고 다음 Agent를 결정한다 03_supervisor-and-routing 에서는 Supervisor가 현재 State를 확인하고 다음 Worker를 선택했다. 여기서 Supervisor가 판단하는 것은 이전 Agent의 결과가 Contract를 만족하는지가 아니다. “현재 상태에서 다음에는 어떤 Agent가 필요한가?” 를 결정한다. 따라서 Contract가 Agent가 만든 결과 를 본다면, Supervisor는 그 결과가 반영된 현재 Workflow State 를 보고 다음 행동을 결정한다. 같은 '통제'처럼 보였지만 지점이 달랐다 두 실습의 흐름을 연결해서 보니 차이가 더 명확했다. Contract는 Agent의 작업이 끝난 이후의 결과 를 검증하고, Supervisor는 현재까지의 State를 바탕으로 다음에 실행할 Agent 를 결정한다. Contract Supervisor 보는 것 Agent가 만든 결과 현재 Workflow State 판단 결과를 사용해도 되는가? 다음에 어떤 Agent가 필요한가? 시점 Agent 실행 후 다음 Agent를 결정할 때 둘 다 Workflow에 개입하기 때문에 처음에는 비슷한 통제 장치처럼 보였지만, 실제로 통제하는 지점은 달랐다. 그렇다면 둘 중 하나만 있어도 될까? Contract만 있다면 Agent의 결과가 정해진 조건을 만족하는지는 검증할 수 있다. 하지만 Contract가 현재 Workflow를 보고 다음 Agent를 선택하는 것은 아니다. 반대로 Supervisor는 현재 State를 보고 다음 Worker를 결정할 수 있지만, 이전 Worker가 만든 결과가 정해진 Contract를 만족하는지 검증하는 역할과는 다르다. 정리하면, Contract → Agent가 만든 결과를 검증 Supervisor → 현재 State를 보고 다음 행동을 결정 둘 중 하나가 다른 하나를 대신하는 관계가 아니라 Multi-Agent Workflow의 서로 다른 지점을 담당하는 역할 이었다. 정리 처음에는 Contract와 Supervisor 모두 Agent를 통제하는 것처럼 보여 역할이 겹치는 부분이 있다고 생각했다. 하지만 실습 흐름에 놓고 비교해보니 차이는 명확했다. Contract는 Agent가 만든 결과를 검증하고, Supervisor는 현재 State를 보고 다음 행동을 결정한다. 결국 Agent를 통제한다 는 표현만으로 이해하기보다, 무엇을 보고 → 무엇을 판단하고 → Workflow의 어느 시점에 개입하는지 구분해서 보는 것이 두 역할을 이해하는 데 더 정확했다. 비슷해 보였던 두 개념을 연결해서 생각해보면서, Multi-Agent Workflow에서는 하나의 Agent가 모든 것을 결정하는 것이 아니라 결과 검증과 다음 행동 결정처럼 서로 다른 책임을 나누어 관리한다는 점 을 이해할 수 있었다. GitHub 이번 내용과 관련된 실습 코드는 GitHub에서 확인할 수 있다. 📂 Agent Role & Contract https://github.com/jbbdyee/aidevs/tree/main/07_multi-agent-service-ops/02_agent-role-and-contract 📂 Supervisor & Routing https://github.com/jbbdyee/aidevs/tree/main/07_multi-agent-service-ops/03_supervisor-and-routing