#FAFAFA 13개의 스레드 ✕ 해제
이온디
이온디 1개월 전
시리즈 ← 이전 편 바이브코딩의 위험성 ③ — 사람은 잊지만 코드는 잊지 않는다 사고는 위험한 자유의 부산물이 아니라, 안전망 부재의 결과다. — 시리즈 3편 / 회고와 안전망 설계 바이브코딩의 양면성 "바이브코딩(vibe coding)"이라는 말이 유행어가 되었다. AI에게 분위기와 의도만 던져주면 코드가 술술 나오는, 자연어로 개발하는 시대. 코드 한 줄 모르는 사람도 며칠이면 자기 사이트를 만든다. 우리는 더 이상 if-else나 for-loop을 외울 필요가 없다. 그 자리에 들… 시리즈 ← 이전 편 바이브코딩의 위험성 ③ — 사람은 잊지만 코드는 잊지 않는다 사고는 위험한 자유의 부산물이 아니라, 안전망 부재의 결과다. — 시리즈 3편 / 회고와 안전망 설계 바이브코딩의 양면성 "바이브코딩(vibe coding)"이라는 말이 유행어가 되었다. AI에게 분위기와 의도만 던져주면 코드가 술술 나오는, 자연어로 개발하는 시대. 코드 한 줄 모르는 사람도 며칠이면 자기 사이트를 만든다. 우리는 더 이상 if-else나 for-loop을 외울 필요가 없다. 그 자리에 들어선 건 기획·아이디어·도메인 이해다. 좋은 시대가 온 게 맞다. 그런데 좋은 시대는 새로운 종류의 사고도 함께 들고 온다. 이번 사고가 바로 그 예시였다. 평범한 모델 파일 하나를 추가하면서, alembic env.py에 import 한 줄을 추가하지 않았다. 그것 하나로 1091줄짜리 폭탄 마이그레이션이 만들어졌다. 사람은 한 줄을 잊었을 뿐인데, 자동화 도구는 그 망각을 30개 테이블의 DROP 문으로 증폭시켰다. AI 도구든 alembic 같은 자동화 스크립트든, 자동화는 인간의 실수를 대신 봐주지 않는다. 오히려 증폭시킨다. "조심하자"는 답이 아니다 가장 흔한 사후 대처는 "다음부터 조심하자"다. 그리고 가장 자주 실패하는 대처도 "다음부터 조심하자"다. env.py에 import를 빠뜨렸을 때 누구도 일부러 그런 게 아니다. 새 모델 추가하느라 정신없는 와중에, env.py라는 한참 떨어진 파일을 동시에 떠올리는 게 어려웠을 뿐이다. 인간은 작업 중에 컨텍스트를 좁힌다. 그게 결함이 아니라 본성이다. 그래서 답은 사람의 주의력에 의존하지 않는 구조다. 인간이 잊을 수 있는 자리를 자동화로 채우는 것. 이번 사고에서 우리가 한 일은 사실 그 한 가지였다. for _info in pkgutil.iter_modules(_models_pkg.__path__): if _info.name.startswith("_") or _info.name == "base": continue importlib.import_module(f"app.models.{_info.name}") 이 6줄이 의미하는 바는 단순하다. 누구도 더 이상 import를 까먹을 수 없다. 모델 파일이 디렉터리에 있으면 자동으로 잡힌다. 잊을 수 있는 자리 자체를 없앤 것이다. 3중 방어 — 사고를 몇 단계에서 막을 것인가 좋은 시스템은 한 군데에서 막는 게 아니라, 여러 군데에서 같은 사고를 잡는다. 첫 번째 방어선이 뚫려도 두 번째가 있고, 두 번째가 뚫려도 세 번째가 있어야 한다. 이번 일을 계기로 마이그레이션 사고에 대한 3중 방어를 설계했다. 1차 방어 — 모델 메타데이터 자동 등록 방금 본 그것. 누락 자체가 일어나지 않도록 구조를 바꾼다. 2차 방어 — Pre-commit gate 혹시라도 1차가 뚫려서 위험한 마이그레이션이 만들어졌다면, commit 단계에서 차단한다. 신규 마이그레이션 파일에 drop_table이나 drop_index가 일정 개수 이상이면 git이 commit을 거부하도록 한다. # .git/hooks/pre-commit NEW_MIGRATIONS=$(git diff --cached --name-only --diff-filter=A | grep '^alembic/versions/.*\.py$') for f in $NEW_MIGRATIONS; do DROPS=$(grep -cE "drop_table|drop_index" "$f") if [ "$DROPS" -ge 3 ]; then echo "❌ $f 에 drop 작업이 $DROPS 건 있습니다. 의도된 변경인지 확인하고 --no-verify로 우회하세요." exit 1 fi done 3차 방어 — Pre-deploy gate 다른 사람이 다른 환경에서 commit한 게 production까지 흘러왔다고 해도, 배포 직전에 한 번 더 막는다. 배포 스크립트(server.sh)에서 alembic upgrade를 돌리기 전에 dry-run을 먼저 시킨다. SQL=$(alembic upgrade --sql current:head) if echo "$SQL" | grep -qE "DROP TABLE|TRUNCATE"; then echo "❌ 배포 중단 — 위험한 SQL 감지" exit 1 fi alembic upgrade head 이 세 단계 중 어느 한 곳도 사람의 주의력에 의존하지 않는다. 자고 있어도, 출근길에 정신없어도, 이미 쳤던 명령을 의식 없이 다시 쳐도 — 안 터진다. 백업 자동화와 binlog 이번 사고에서 가장 손발이 묶였던 순간은 binlog가 OFF라는 걸 확인했을 때였다. 점-시간 복구라는, 데이터베이스의 마지막 안전망이 처음부터 없었던 것이다. binlog는 MySQL의 변경 이력을 별도 파일에 기록하는 기능이다. 이게 켜져 있으면 "어제 14:50:59 시점으로 복구해줘"가 가능하다. 꺼져 있으면 그냥 마지막 백업 시점까지밖에 못 돌아간다. 켜는 건 한 줄이다. /etc/mysql/mysql.conf.d/mysqld.cnf에: log_bin = /var/log/mysql/mysql-bin.log expire_logs_days = 7 expire_logs_days = 7이 중요하다. 안 적으면 binlog가 무한히 쌓여서 디스크가 꽉 찬다. 7일이면 보통의 사고 추적엔 충분하고, 디스크 부담도 적다. 그리고 binlog가 있어도 풀 백업은 따로 필요하다. 복구는 "풀 백업 + 그 이후의 binlog" 조합으로 이뤄지기 때문이다. 풀 백업은 cron으로 자동화한다. # 매일 새벽 3시 백업 0 3 * * * /home/.../scripts/db_backup.sh >> /home/.../private/backup.log 2>&1 # 매일 새벽 3시 30분, 14일 이전 백업 자동 삭제 30 3 * * * find /home/.../private/ -name '*-*.sql.gz' -mtime +14 -delete 이 두 줄이면 매일 새벽 백업이 만들어지고, 14일 이전 백업이 자동 정리된다. 한 번 설정하면 잊고 살아도 된다. 디테일이 안전을 만든다 이번 작업에서 사소해 보이지만 중요했던 디테일들을 적어둔다. set -euo pipefail — 백업 스크립트 첫 줄. 한 줄이라도 실패하면 즉시 멈춘다. 이게 없으면 mysqldump가 중간에 실패해도 gzip은 부분 데이터를 압축하면서 "성공" 종료 코드를 반환할 수 있다. 부분 백업은 백업이 아니다. 이 한 줄이 그 차이를 만든다. --single-transaction — InnoDB에서 락 없이 일관된 스냅샷을 떠준다. production 트래픽에 영향을 거의 안 주면서 데이터 일관성을 보장. --routines --triggers --events — 이걸 빼먹으면 트리거·뷰·저장 프로시저가 백업에 안 들어간다. 복원 후에 알 수 없는 동작이 사라진 채로 운영되는 상황이 생긴다. OUT_DIR="$HOME/web/.../private" — 절대 경로 대신 $HOME을 쓰면 다른 서버나 다른 계정에서도 그대로 동작한다. 그리고 백업 위치가 웹에서 노출되지 않는 디렉터리인지 반드시 확인한다. chmod +x를 로컬에서 미리 — rsync는 파일 권한을 그대로 복사한다. 로컬에서 +x를 안 했으면 production에서 또 chmod를 해야 한다. 작은 일이지만, 매번 잊는 종류의 일이다. ~/.ssh/config 별칭 등록 — 매번 풀 도메인 치는 대신 ssh mars 한 단어로 접속. 작은 편의지만, 이런 작은 마찰이 쌓이면 보안 작업도 귀찮아진다. (NEUTRALIZED) 같은 명시적 표시 — 비운 마이그레이션의 docstring 첫 줄에 이렇게 넣어두면, dry-run 출력에도 그대로 떠서 "지금 처리되는 게 무력화된 파일이 맞다"는 즉각적인 시각 확인이 된다. 6개월 뒤의 자기 자신을 위한 친절. 이런 디테일들은 하나하나는 사소하지만, 사고 한가운데에서 마음을 가라앉히는 건 결국 이런 작은 확실성들이다. 다 끝난 뒤에 남은 것 이번 사고로 잃은 데이터는 단순 로그 정도였고 다시 채울 수 있는 수준이었다. 하지만 만약 폭탄이 발견되지 않은 채 다음 배포 때 터졌다면? 그땐 30개 테이블이 진짜로 사라졌을 것이다. 그중에는 단순 로그만 있는 게 아니었다 — 고객 관리, 마케팅 캠페인, 뉴스레터 구독자, 게시판 SEO 설정 같은, 다시 채우려면 외부 자료 없이는 불가능한 데이터들이 섞여 있었다. 운이 좋았다. 그런데 운이 좋았다는 건 시스템이 안전했다는 게 아니라, 단지 이번엔 누군가 일찍 알아챘다는 뜻일 뿐이다. 다음 사고는 늦게 발견될 수도 있다. 자동화의 시대에는, 사고가 너무 빨리 너무 광범위하게 퍼진다. 알아챈 시점엔 이미 늦은 경우가 많다. 그래서 우리는 안전망을 코드에 박아둔다. env.py의 자동 import, pre-commit hook, pre-deploy gate, 매일 백업, binlog. 사람은 잊지만 코드는 잊지 않으니까. 바이브코딩이 위험한 게 아니다. 안전망 없는 자동화가 위험한 것이다. 자연어로 개발하는 시대에, 우리가 새로 배워야 할 건 아마 코드 그 자체가 아니라 — 자동화의 사각지대를 미리 메우는 감각일 것이다. 당신의 바이브코딩은 안전한가요? AI에게 코드를 맡겼다면, 안전망도 같이 맡겼는지 한 번 점검해보세요. 7가지 항목으로 끝나는 무료 자가 진단. 바이브코딩 안전 체크리스트 받기 → 시리즈 끝. 다음에는 더 안전한 코드로 만나길.
이온디
이온디 1개월 전
시리즈 ← 이전 편 · 다음 편 → 바이브코딩의 위험성 ② — 범인은 autogenerate였다 1091줄짜리 시한폭탄을 만든 건 사람이 아니라 자동화 도구 자신이었다. — 시리즈 2편 / 진짜 원인 발견과 해체 작업 Alembic autogenerate가 그러는 이유 Alembic은 SQLAlchemy 기반 프로젝트의 마이그레이션 도구다. alembic revision --autogenerate라는 명령을 치면, 현재 모델 정의(SQLAlchemy Base.metadata)와 실제… 시리즈 ← 이전 편 · 다음 편 → 바이브코딩의 위험성 ② — 범인은 autogenerate였다 1091줄짜리 시한폭탄을 만든 건 사람이 아니라 자동화 도구 자신이었다. — 시리즈 2편 / 진짜 원인 발견과 해체 작업 Alembic autogenerate가 그러는 이유 Alembic은 SQLAlchemy 기반 프로젝트의 마이그레이션 도구다. alembic revision --autogenerate라는 명령을 치면, 현재 모델 정의(SQLAlchemy Base.metadata)와 실제 DB 스키마를 비교해서 그 차이를 마이그레이션 파일로 자동 생성해준다. 컬럼 추가, 인덱스 추가, 테이블 추가 같은 변경을 사람이 손으로 SQL을 적지 않아도 되게 해주는 편리한 도구다. 문제는 이 비교의 방향성이다. Alembic은 "모델에는 있는데 DB에 없는 것"을 추가 작업으로 인식하고, 동시에 "DB에는 있는데 모델에 없는 것"을 삭제 작업으로 인식한다. 후자가 함정이다. Base.metadata에 어떤 모델이 등록되지 않은 상태에서 autogenerate를 돌리면, 실제로는 코드 어딘가에 살아 있는 모델이라도 alembic 입장에선 "사라진 테이블"이 된다. 그러면 친절하게 op.drop_table('...')을 자동으로 만들어준다. alembic/env.py를 열어봤다. from app.models import document, member, comment, file, module, site_config, site from app.models import hosting_site, hosting_subscription, project, inquiry, member_profile from app.models import sale_product, order, revenue, settlement from app.models import project_issue, kakao_chat, project_billing, project_file, project_comment 손으로 적은 import가 21개. 그런데 app/models/ 디렉터리에는 모델 파일이 57개. 빠진 36개의 정체: audit_log.py bank_transaction.py blog_post.py client.py hosting_setup_log.py marketing.py notification_log.py spam.py wiki.py analytics_report.py module_group.py newsletter.py ... 폭탄 마이그레이션이 DROP하려던 그 테이블들이, 정확히 env.py에서 import 누락된 모델 파일들과 일치했다. .title { font-size: 22px; font-weight: 700; fill: #111; } .subtitle { font-size: 14px; fill: #666; } .label { font-size: 14px; fill: #222; font-weight: 500; } .num { font-size: 28px; font-weight: 700; } .small { font-size: 12px; fill: #555; } .bad { fill: #c83737; } .good { fill: #1f7a3f; } .arrow { stroke: #777; stroke-width: 1.6; fill: none; } .arrow-bad { stroke: #c83737; stroke-width: 1.8; fill: none; stroke-dasharray: 4 3; } .arrow-good { stroke: #1f7a3f; stroke-width: 1.8; fill: none; } .box { stroke: #ccc; stroke-width: 1; fill: #fff; rx: 8; } .box-bad { stroke: #e0a8a8; stroke-width: 1; fill: #fff5f5; rx: 8; } .box-good { stroke: #a8d4b8; stroke-width: 1; fill: #f3faf5; rx: 8; } .row-label{ font-size: 13px; fill: #888; font-weight: 600; letter-spacing: 1px; } env.py의 침묵하는 누락 손으로 적은 import 21개 vs 디렉터리에 실재하는 모델 57개 BEFORE — 사고 시점 app/models/ 57 실재 모델 파일 env.py 21 손으로 적은 import Base.metadata 21 등록된 테이블 autogenerate −36 DROP TABLE 자동 생성 metadata에 없는 36개 모델은 alembic 입장에서 "사라진 테이블"로 보임 → 1091줄짜리 폭탄 마이그레이션 파일이 자동 생성됨 AFTER — env.py 자동 import 적용 app/models/ 57 실재 모델 파일 env.py auto pkgutil.iter_modules Base.metadata 78 전체 자동 등록 autogenerate 0 false-DROP 차단 새 모델 파일을 추가해도 env.py에 손대지 않아도 됨 — 잊을 수 있는 자리 자체가 사라짐 env.py에 손으로 적은 21개 import vs 디렉터리에 실재하는 57개 모델 — 그 격차가 폭탄을 만든다. 원인 확정. 사람의 게으름이 아니라, 시스템 설계의 문제였다. 새 모델을 추가할 때마다 env.py에 한 줄을 손으로 더 적어야 하는 구조 자체가, 언젠가 누락이 생길 시한폭탄이었다. 백업이 가장 먼저 원인을 알았다고 해서 바로 수정 작업에 들어가면 안 된다. binlog가 꺼진 상태에서, 잘못된 한 줄이 더 큰 사고를 만들 수도 있다. 이번 작업의 모든 안전성은 현재 시점 백업 한 장에 달려 있었다. 백업 스크립트를 짜기 전에 한 가지 고려사항이 있었다 — eondcms는 트래픽이 꽤 있는 사이트라 카운터·통계·API 호출 로그 테이블 3개가 매우 크다. 이걸 그대로 풀 덤프하면 시간도 오래 걸리고 용량도 부담스럽다. 이 3개는 데이터는 빼고 스키마만 보존하기로 했다. 복원 후에도 빈 테이블 껍데기는 만들어져야 ORM이 INSERT를 시도할 때 에러가 나지 않으니까. 핵심 패턴은 단순하다. mysqldump를 두 번 호출해서 stdout을 이어붙인 뒤 한 번에 gzip으로 압축한다. 결과물은 단일 파일. { mysqldump --single-transaction --routines --triggers --events \ --default-character-set=utf8mb4 --hex-blob \ --ignore-table=$DB.xe_counter_log \ --ignore-table=$DB.xe_api_call_logs \ --ignore-table=$DB.xe_stats_log \ "$DB" mysqldump --no-data --default-character-set=utf8mb4 \ "$DB" xe_counter_log xe_api_call_logs xe_stats_log } | gzip > "$OUT_FILE" 스크립트 첫 줄에 set -euo pipefail을 박아두는 게 중요하다. 한 줄이라도 실패하면 즉시 멈추도록. 백업이 도중에 깨졌는데 "성공"이라고 착각하는 사고가 가장 흔하니까. 결과물은 압축 후 37MB. 압축 전 원본 기준 약 300~450MB 추정. 이걸 웹에서 절대 보이지 않는 디렉터리에 저장하는 것도 중요했다. 백업 파일에는 비밀번호 해시·세션 토큰·이메일 같은 민감정보가 그대로 들어 있어서, "private"이라는 이름의 디렉터리에 둔다고 해도 실제로 웹서버 설정이 막아주지 않으면 누구나 다운로드 가능하다. env.py 자동화 — 사람의 주의력에 의존하지 않기 이제 진짜 수정. env.py를 손으로 import 목록을 유지하는 방식이 사고의 근본 원인이라면, 답은 자동 import다. 새 모델 파일이 추가될 때마다 자동으로 등록되도록 바꾸면, 누구도 손으로 적는 걸 까먹을 수 없다. import importlib import pkgutil import app.models as _models_pkg for _info in pkgutil.iter_modules(_models_pkg.__path__): if _info.name.startswith("_") or _info.name == "base": continue importlib.import_module(f"app.models.{_info.name}") pkgutil.iter_modules는 패키지 안의 모든 모듈을 순회해주는 표준 라이브러리. 베이스 모듈만 제외하고 전부 import한다. 검증 결과 55개 모듈이 자동 로드되어 78개 테이블이 metadata에 등록되었다. 이전 21개 import로 잡지 못했던 36개 모델이 이번에 모두 합류했다. 이 변경 한 줄이 의미하는 바는 명확하다. 앞으로 누가 새 모델 파일을 만들어도 env.py를 손대지 않아도 된다. autogenerate가 잘못된 DROP을 만들 가능성이 구조적으로 차단된다. 폭탄 마이그레이션 무력화 남은 일은 1091줄짜리 폭탄을 어떻게 처리할 것인가였다. 선택지는 두 개: A. 파일 자체를 삭제 — 깔끔해 보이지만, alembic 입장에선 chain의 한 노드가 사라지는 것이라 후속 마이그레이션이 깨진다. B. 내용만 비우기 — revision과 down_revision 필드는 그대로 두고, upgrade()와 downgrade() 함수 본문을 pass로 교체한다. chain은 그대로, 동작만 무력화. B를 골랐다. 사고 경위와 신원 보존이 되는 주석을 헤더에 적어두고: """add_random_order_to_board_config (NEUTRALIZED) ⚠️ 이 마이그레이션은 의도적으로 비워졌습니다 (2026-04-28). 사고 경위: alembic env.py가 app/models/ 아래 일부 모델만 import하던 상태에서 --autogenerate 가 실행되어, 누락된 36개 모델이 "사라진 테이블"로 인식 → 30+ 테이블 DROP을 자동 생성한 1091줄짜리 폭탄 마이그레이션이 됨. """ def upgrade() -> None: pass def downgrade() -> None: pass 이 주석은 미래의 누군가가 — 6개월 뒤의 자기 자신을 포함해서 — "왜 이 파일이 비어있지?" 라고 물었을 때, 짧은 단서가 되어줄 것이다. 코드 자체가 자기 역사를 설명할 수 있어야 한다. 안전한 적용 절차 수정한 파일들을 production에 보내기 전, 한 가지 더 확인할 게 있었다. dry-run. alembic upgrade --sql은 offline 모드로 동작해서, DB에 연결조차 하지 않고 실행될 SQL을 텍스트로만 출력한다. 진짜 실행 전에 무엇이 production에서 일어날지 미리 보여주는 안전 기능이다. alembic upgrade --sql c3d4e5f6a1b2:head | grep -E "DROP TABLE|TRUNCATE" \ && echo "❌ 아직 위험" \ || echo "✅ no destructive ops" grep이 무언가 잡히면 → 위험. 아무것도 안 나오면 → 안전. 아주 단순하지만 마음 편한 검증이다. 처음 production에서 dry-run을 돌렸을 때는 DROP 문이 좌라락 출력되었다. 잠깐 가슴이 철렁했지만, 곧 이유를 알았다 — 우리가 로컬에서 비운 두 파일이 production에는 아직 안 갔던 것. rsync로 동기화 후 재실행: Running upgrade c3d4e5f6a1b2 -> a6ae466f8b07, add_random_order_to_board_config (NEUTRALIZED) ✅ no destructive ops (NEUTRALIZED) 표시가 떠 있는 게 결정적 증거였다. production이 우리가 비운 새 파일을 정상적으로 읽고 있다는 뜻. 이제 진짜로 적용해도 안전하다. $ alembic upgrade head INFO Running upgrade c3d4e5f6a1b2 -> a6ae466f8b07, add_random_order_to_board_config (NEUTRALIZED) $ alembic current a6ae466f8b07 (head) DB 변경 0건, 데이터 손실 0건, 다운타임 0초. alembic_version 테이블의 한 행만 갱신되었다. 폭탄 해체 완료. 다음 편 — ③ 바이브코딩의 위험과 안전망 — 사람은 잊지만 코드는 잊지 않는다
이온디
이온디 1개월 전
시리즈 다음 편 → 바이브코딩의 위험성 ① — 어제는 분명히 괜찮았다 AI에게 코드를 맡기는 시대, 우리는 어떤 안전망을 가지고 있을까? 한 번의 자동화가 30개 테이블을 날릴 뻔한 이야기. — 시리즈 1편 / 사건 발견과 추적 평범한 아침, 작은 위화감 오늘 아침 관리자 화면에 접속했더니 몇몇 메뉴가 비어 있었다. SEO 설정, 재무 거래내역, 호스팅 생성 로그. 그리고 발송 로그도. 처음에는 단순히 "아직 데이터를 안 채워서 그런가 보다" 싶었다. 신규 기능 위주의 메뉴들이라서 그럴… 시리즈 다음 편 → 바이브코딩의 위험성 ① — 어제는 분명히 괜찮았다 AI에게 코드를 맡기는 시대, 우리는 어떤 안전망을 가지고 있을까? 한 번의 자동화가 30개 테이블을 날릴 뻔한 이야기. — 시리즈 1편 / 사건 발견과 추적 평범한 아침, 작은 위화감 오늘 아침 관리자 화면에 접속했더니 몇몇 메뉴가 비어 있었다. SEO 설정, 재무 거래내역, 호스팅 생성 로그. 그리고 발송 로그도. 처음에는 단순히 "아직 데이터를 안 채워서 그런가 보다" 싶었다. 신규 기능 위주의 메뉴들이라서 그럴 만도 했다. 그런데 이상했다. 며칠 전 분명히 본 적이 있는 데이터들이었다. 이게 왜 비어 있지? 처음 떠올린 가설은 단순했다. eondcms는 Rhymix(xe_* 테이블)를 그대로 쓰면서 신규 기능은 eond_* 테이블에 적재하는 구조다. 신규 테이블이 새로 만들어진 거라, 이전 데이터가 없는 게 자연스럽다. 깔끔한 설명이었다. 그런데 깔끔한 설명일수록 의심해야 한다는 걸, 8년 프리랜서 생활이 가르쳐준 본능이 있었다. "원래 있었다"는 증언 테이블 행 수를 직접 세어 봤다. eond_site — 4개 행. SEO는 사이트별로 채우면 되는 정상 상태. eond_bank_transactions — 0개. 이전엔 있었다. eond_hosting_setup_log — 0개. 이것도 있었다. "원래 있었다"는 기억이 옳다면, 신규 테이블 가설은 무너진다. 데이터가 사라진 것이다. 이쯤에서 가장 먼저 점검하는 건 MySQL의 binlog. 점-시간 복구(point-in-time recovery)가 가능한 유일한 안전망이다. SHOW VARIABLES LIKE 'log_bin'; OFF. 이 한 줄로 모든 복구 옵션이 사라졌다. 백업 디렉터리도 비어 있었다. 추적할 수 있는 흔적이 통째로 사라진 셈이다. 어제는 정상이었다 다행인지 불행인지, 사라진 건 "단순 로그 정도"였다. 글, 회원, 댓글, 파일 같은 핵심 데이터는 멀쩡했다. 다시 채워 넣으면 되는 수준. 그렇다고 그냥 넘어갈 일은 아니었다. 이런 사고는 다음에도 일어날 수 있고, 다음엔 운이 안 좋을 수도 있으니까. 추적의 단서를 찾기 위해 시점부터 좁혔다. 어제는 괜찮았던 걸로 기억함 이 한 줄이 결정적이었다. 어제까지 정상이었고, 오늘 사라졌다. 그렇다면 그 사이에 누군가 무엇을 했다. git log를 펼쳤다. efd252f 2026-04-28 14:51:03 feat: major update — inquiry-to-project flow, notification logging, market price, xe_ table integration 오늘 오후 2시 51분에 거대한 커밋이 하나 들어가 있었다. "major update"라는 말이 이미 의심스러웠지만, 그보다 눈에 띈 건 변경된 파일 수였다. 모델 추가, 라우터 추가, 서비스 추가, 마이그레이션 파일들 — 그중에서 한 줄이 유독 튀었다. 1091 +++ alembic/versions/a6ae466f8b07_add_random_order_to_board_config.py 마이그레이션 파일 하나가 1091줄. 게다가 이름은 "board_config에 random_order 추가". 컬럼 하나 추가하는 데 1091줄? 이름과 다른 내용 파일을 열어봤다. """add_random_order_to_board_config Revision ID: a6ae466f8b07 Create Date: 2026-04-27 16:36:16.654089 """ def upgrade() -> None: op.create_table('eond_hosting_plan', ...) op.drop_table('eond_project_quote_items') op.drop_table('eond_outreach_template') op.drop_table('eond_notification_log') op.drop_table('eond_marketing_activity') ... 이름은 컬럼 추가인데, 내용은 30개가 넘는 테이블을 DROP하는 코드였다. 사라진 테이블 목록을 훑어보니 정확히 오늘 사라진 데이터들이 거기에 있었다. eond_bank_transactions (재무 거래내역) ✓ eond_hosting_setup_log (호스팅 생성 로그) ✓ eond_clients (고객 관리) ✓ eond_audit_log, eond_blog_posts, eond_member_login_log eond_marketing_* 시리즈 거의 전부 eond_newsletter_*, eond_post_seo, eond_board_seo eond_publish_queue, eond_writing_material, eond_wiki_pages ... 범인을 찾았다고 생각했다. 어제(4/27 16:36) 만들어진 이 파일이, 오늘 배포(4/28 14:51) 시점에 production에서 실행되며 데이터를 다 날려버렸다고. 시간 순서도 완벽하게 맞아떨어졌다. 그런데 정말 그랬을까? 결정적 한 줄 확신을 굳히기 전에 production에서 alembic 상태를 확인했다. $ alembic current c3d4e5f6a1b2 이 한 줄이 모든 추리를 뒤집었다. c3d4e5f6a1b2는 폭탄 마이그레이션(a6ae466f8b07)의 바로 직전 단계였다. 즉 production은 아직 폭탄을 실행하지 않은 상태였다. 1091줄의 DROP 문은 production DB에 한 줄도 닿지 않았다. 그렇다면 오늘 비어버린 데이터는 누가 지웠나? 솔직히 말하자면 — 모른다. binlog가 꺼져 있어서 추적할 흔적이 없다. 어쩌면 어떤 수동 작업, 어쩌면 다른 환경과의 혼동, 어쩌면 우리가 모르는 어떤 reset 스크립트의 흔적. 영원히 미궁이다. 하지만 진짜 무서운 건 그게 아니다. 폭탄은 아직 안 터졌을 뿐, 신관은 빠져 있는 상태로 차에 실려 있다는 것. 다음 배포 때 누가 무심코 alembic upgrade head를 누르는 순간, 그제서야 30개 테이블이 진짜로 사라진다. 이번엔 운 좋게 발견했지만, 발견하지 못했다면? 이 시점부터 작업의 성격이 바뀌었다. 사고 조사가 아니라 사고 예방으로. 다음 편 — ② 범인은 autogenerate였다 — 폭탄 마이그레이션 해체
이온디
이온디 11년 전
사용시 주의점 : 1. 스크립트 호출 순서를 지켜주세요. 호출 순서가 달라지면 소스가 적용이 되지 않습니다. 2. 스크립트 위치는 상관이 없습니다. 헤더 안 혹은 닫히는 바디 바로 앞에 두어도 동작은 합니다. 참조 사이트 : 1. 조은벗들 <script type="text/javascript"> // 달력 선택 function makeDate(dayDiff ) { var curDate = new Date( ); var diffDate … 사용시 주의점 : 1. 스크립트 호출 순서를 지켜주세요. 호출 순서가 달라지면 소스가 적용이 되지 않습니다. 2. 스크립트 위치는 상관이 없습니다. 헤더 안 혹은 닫히는 바디 바로 앞에 두어도 동작은 합니다. 참조 사이트 : 1. 조은벗들 <script type="text/javascript"> // 달력 선택 function makeDate(dayDiff ) { var curDate = new Date( ); var diffDate = new Date(curDate.getFullYear(),curDate.getMonth(),curDate.getDate() ); diffDate.setDate(diffDate.getDate() + dayDiff); var strCurDate= curDate.getFullYear() +'-'+ (curDate.getMonth()+101 +"").substring(1,3) +'-'+(curDate.getDate()+100 +"").substring(1,3); var strDiffDate= diffDate.getFullYear() +'-'+ (diffDate.getMonth()+101 +"").substring(1,3) +'-'+(diffDate.getDate()+100 +"").substring(1,3); frmTest.fldDate2.value=strCurDate; frmTest.fldDate1.value=strDiffDate; } $(function(){ $.datepicker.regional['ko'] = { closeText: '닫기', prevText: '이전달', nextText: '다음달', currentText: '오늘', monthNames: ['1월(JAN)','2월(FEB)','3월(MAR)','4월(APR)','5월(MAY)','6월(JUN)', '7월(JUL)','8월(AUG)','9월(SEP)','10월(OCT)','11월(NOV)','12월(DEC)'], monthNamesShort: ['1월','2월','3월','4월','5월','6월', '7월','8월','9월','10월','11월','12월'], dayNames: ['일','월','화','수','목','금','토'], dayNamesShort: ['일','월','화','수','목','금','토'], dayNamesMin: ['일','월','화','수','목','금','토'], weekHeader: 'Wk', dateFormat: 'yy-mm-dd', firstDay: 0, isRTL: false, showMonthAfterYear: true, yearSuffix: ''}; $.datepicker.setDefaults($.datepicker.regional['ko']); $('#selecter1').datepicker({ //showOn: 'button', buttonImage: '/cal.png', //이미지 url buttonImageOnly: true, buttonText: "달력", changeMonth: true, changeYear: true, showButtonPanel: true }); $('#selecter2').datepicker({ //showOn: 'button', buttonImage: '/cal.png', //이미지 url buttonImageOnly: true, buttonText: "달력", changeMonth: true, changeYear: true, showButtonPanel: true }); }); </script> 1. http://blog.naver.com/qfwh/220018487871 <script src="/js/jquery-1.8.3.min.js"></script> <link href="/jquery-ui.css" rel="stylesheet" /> <script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script> <script> $(function(){ $.datepicker.regional['ko'] = { closeText: '닫기', prevText: '이전달', nextText: '다음달', currentText: '오늘', monthNames: ['1월(JAN)','2월(FEB)','3월(MAR)','4월(APR)','5월(MAY)','6월(JUN)', '7월(JUL)','8월(AUG)','9월(SEP)','10월(OCT)','11월(NOV)','12월(DEC)'], monthNamesShort: ['1월','2월','3월','4월','5월','6월', '7월','8월','9월','10월','11월','12월'], dayNames: ['일','월','화','수','목','금','토'], dayNamesShort: ['일','월','화','수','목','금','토'], dayNamesMin: ['일','월','화','수','목','금','토'], weekHeader: 'Wk', dateFormat: 'yy-mm-dd', firstDay: 0, isRTL: false, showMonthAfterYear: true, yearSuffix: ''}; $.datepicker.setDefaults($.datepicker.regional['ko']); $('#selecter').datepicker({ showOn: 'button', buttonImage: '/cal.png', //이미지 url buttonImageOnly: true, buttonText: "달력", changeMonth: true, changeYear: true, showButtonPanel: true }); }); </script> <input type="text" id="selecter" name="selecter" /> [출처] (JQuery) 달력 JQuery 'Datepicker'|작성자 푸른인연 2. jQuery UI: Adding a trigger button for Datepicker [추천]버튼을 이용한 데이트픽커 http://www.phpeveryday.com/articles/jQuery-UI-Adding-a-trigger-button-for-Datepicker-P1020.html jQuery UI Adding a trigger button for Datepicker tutorial. By default, user can open datepicker when <input> element receives focus. we can change this very easily. We can make datepicker opens when a button is clicked instead. The most basic type of <button> can be enabled with just the showOn option $(function(){ var pickerOpts = { showOn: "button" }; $("#date").datepicker(pickerOpts);}); Complete Example Code: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html lang="en"> <head> <link rel="stylesheet" type="text/css" href="development-bundle/themes/ui-lightness/jquery.ui.all.css"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>jQuery UI Datepicker Example 1</title> <script type="text/javascript" src="development-bundle/jquery-1.4.2.js"></script> <script type="text/javascript" src="development-bundle/ui/jquery.ui.core.js"></script> <script type="text/javascript" src="development-bundle/ui/jquery.ui.datepicker.js"></script> <script type="text/javascript"> $(function(){ var pickerOpts = { showOn: "button" }; $("#date").datepicker(pickerOpts); }); </script> </head> <body> <label>Enter a date: </label><input id="date"> </body> </html> We can change the default text that show on the button. just providing a new string as the value of the buttonText option: $(function(){ var pickerOpts = { showOn: "button", buttonText: "Show Datepicker" }; $("#date").datepicker(pickerOpts); }); $(function(){ var pickerOpts = { showOn: "button", buttonImage: "img/datepicker/date.png", buttonText: "Show Datepicker" }; $("#date").datepicker(pickerOpts); });
이온디
이온디 11년 전
자바스크립트를 이용하여 DIV를 셀렉트박스 형태로 만드는 소스입니다. 2013년 3월에 만든 이온디 포털 레이아웃에 사용된 소스인데, 너무 오래 되서 출처는 모르겠습니다. 저 이미지 자체로 배경그림으로 이뤄져있어서 이미지는 만들기 나름입니다. http://codepen.io/eond/details/rVWyPV/ 1. html <div id="family_site"> <h4 onclick="slideFamilySite()"><span>Family… 자바스크립트를 이용하여 DIV를 셀렉트박스 형태로 만드는 소스입니다. 2013년 3월에 만든 이온디 포털 레이아웃에 사용된 소스인데, 너무 오래 되서 출처는 모르겠습니다. 저 이미지 자체로 배경그림으로 이뤄져있어서 이미지는 만들기 나름입니다. http://codepen.io/eond/details/rVWyPV/ 1. html <div id="family_site"> <h4 onclick="slideFamilySite()"><span>Family Site</span></h4> <div id="site_list"> <ul> <li><a href="#" onclick="window.open(this.href);return false;">이온디</a></li> <li><a href="#" onclick="window.open(this.href);return false;">이온디</a></li> <li><a href="#" onclick="window.open(this.href);return false;">이온디</a></li> <li><a href="#" onclick="window.open(this.href);return false;">이온디</a></li> <li><a href="#" onclick="window.open(this.href);return false;">이온디</a></li> <li><a href="#" onclick="window.open(this.href);return false;">이온디</a></li> </ul> </div> </div> 2. CSS #family_site { position: relative; float:left; top: 7px; left: -10px; padding:0px; background: url('http://eond.com/layouts/eond_portal_main_2col_right_bb/images/default/bg_family_site.gif') no-repeat; } #family_site h4 { cursor: pointer; width: 107px; height: 16px; margin: 0; } #family_site span { display: none; } #family_site #site_list { position: absolute; bottom: 15px; border: solid #d5d5d5; border-width:1px 1px 0 1px; background-color: #fff; width: 105px; height: 0; margin:0; overflow: auto; } #family_site #site_list a:hover, #family_site #site_list a:focus {color:#0066cc; text-decoration:none;} #family_site #site_list ul { list-style: none; margin:0; padding:5px; text-align:left; overflow:hidden; } #family_site #site_list ul li { display:block; color:#666; line-height:14px !important; font:11px "돋움", Dotum; letter-spacing:-1px; } 3. Script // family site function startFamilySiteScroll() { setTimeout("slideFamilySite()", 10); } function slideFamilySite() { el = document.getElementById("site_list"); if (el.heightPos == null || (el.isDone && el.isOn == false)) { el.isDone = false; el.heightPos = 0; el.heightTo = 110; } else if (el.isDone && el.isOn){ el.isDone = false; el.heightTo = 0; } if (Math.abs(el.heightTo - el.heightPos) > 1) { el.heightPos += (el.heightTo - el.heightPos) / 10; el.style.height = el.heightPos + "px"; startFamilySiteScroll(); } else { if (el.heightTo == 110) { el.isOn = true; } else { el.isOn = false; } el.heightPos = el.heightTo; el.style.height = el.heightPos + "px"; el.isDone = true; } }
이온디
이온디 11년 전
#F2F2F2 apple.com 의 백그라운드 컬러 #FAFAFA 일부 영역 #F4F5F6 네이버 밴드의 백그라운드 컬러 #F9F9F9 이온디 온 레이아웃의 백그라운드 컬러 이온디 온의 경우 최대한 흰색을 유지하면서 화이트톤 백그라운드를 유지하고 있다. 애플의 경우는 실버, 그레이 느낌의 백그라운드 톤이다. 애플은 외국 회사 느낌이 나는 차가운 흰색인데 네이버 밴드는 국내사라 그런지 따뜻한 느낌의… #F2F2F2 apple.com 의 백그라운드 컬러 #FAFAFA 일부 영역 #F4F5F6 네이버 밴드의 백그라운드 컬러 #F9F9F9 이온디 온 레이아웃의 백그라운드 컬러 이온디 온의 경우 최대한 흰색을 유지하면서 화이트톤 백그라운드를 유지하고 있다. 애플의 경우는 실버, 그레이 느낌의 백그라운드 톤이다. 애플은 외국 회사 느낌이 나는 차가운 흰색인데 네이버 밴드는 국내사라 그런지 따뜻한 느낌의 흰색을 백그라운드로 사용하였다. #FAFAFA #F2F2F2 #F9F9F9 F4F5F6 이렇게 놓고 비교해보면 일단 네이버의 배경이 따뜻하다고 생각이 들었는데, 두 색을 비교하면 오히려 어두운 색(애플의 배경색)이 더 따뜻한 느낌을 주었다. 하지만 그 색만을 바라보면 애플의 경우는 그레이 느낌이 강하기 때문에 차가운 색이란 생각이 든다. 반면 네이버는 밝은 톤이라 따뜻하게 느껴진 것 같다. (*색이란 빛이나 모니터, 눈이나 환경에 따라 각자 느끼는 바는 다르다.) 이온디 온 레이아웃은 흰색을 주 컬러로 사용하면서 포인트 컬러를 사용했다. 주로 흰색 톤의 색을 군데군데 배치했기에 흰색의 사용이 중요한 몫을 차지하고 있다. 이번에 흰색 배경색을 조사하면서 기존에 F9F9F9 컬러를 FAFAFA로 변경해보았는데, FFF 색과 비교해서 좀 더 확 눈에 띄게 되었지만 눈에 보이는 색 자체는 더 편안해진 것 같다. 탑네비게이션과 컨텐츠의 구분을 할 때, 위젯을 강조할 때 등 흰색의 다양한 버전은 웹디자인에서 굉장히 중요하다. #F4F4F4 : 네이버 카페의 탑네비게이션의 색 #FBFBFB : 네이버 메인의 탑네비게이션의 색 #FAFAFA : 네이버 검색의 탑네비게이션의 색 ※참조 1. 가독성, 눈의 피로함에 대한 웹 사이트 배경색의 영향 http://academic.naver.com/view.nhn?doc_id=13699766&dir_id=0&page=0&query=%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8%20%EB%B0%B0%EA%B2%BD%EC%83%89%20F7f7f7&ndsCategoryId=10204&library=141 2. 실전 웹표준 가이드 http://www.slideshare.net/hyun/web-standard-guide-2005-appendix 3. Designing Interfaces 인터페이스 디자인 94가지 패턴 http://book.naver.com/bookdb/text_view.nhn?bid=2954032&dencrt=K6ydKIG64A7KiKn%252BgOesYB%253%393IzNMfICtDOwSPEEsUqw%253D&term=%BE%D6%C7%C3+%C0%A5+%BB%E7%C0%CC%C6%AE+%B9%E8%B0%E6&query=%EC%95%A0%ED%94%8C+%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8+%EB%B0%B0%EA%B2%BD 4. 웹사이트에 '빽'이 있어야 산다? http://cafe.naver.com/stshanghai/257
이온디
이온디 11년 전
마우스를 올리면 해당 레이어 아래에 있는 특정 요소가 나타났다 사라졌다 하는 소스입니다. 나타나는 작은 삼각형은 div와 css를 이용하여 삼각형을 만들었으며, jquery를 이용하여 마우스 오버 했을 때 나타났다 사라졌다 하게 만들었습니다. 그 외에 레이아웃은 모바일용 레이아웃입니다. (__);; jquery 스크립트 부분입니다. <script type="text/javascript"> /* $("li").hover(function(){ $(this)… 마우스를 올리면 해당 레이어 아래에 있는 특정 요소가 나타났다 사라졌다 하는 소스입니다. 나타나는 작은 삼각형은 div와 css를 이용하여 삼각형을 만들었으며, jquery를 이용하여 마우스 오버 했을 때 나타났다 사라졌다 하게 만들었습니다. 그 외에 레이아웃은 모바일용 레이아웃입니다. (__);; jquery 스크립트 부분입니다. <script type="text/javascript"> /* $("li").hover(function(){ $(this).css("background","#0000ff"); }, function(){ $(this).css("background","#ffffff"); }); */ $( document ).ready( function() { $( '.item' ).hover( function () { //$( '.item > .css-arrow-multicolor' ).css('display','block'); $(this).find(".css-arrow-multicolor").css('display','block'); }, function () { //$( '.item > .css-arrow-multicolor' ).css('display','none'); $(this).find(".css-arrow-multicolor").css('display','none'); } ); } ); </script> css 부분입니다. .main-content .gnb_wrap li .css-arrow-multicolor { display:none; margin: 0 auto; border-color: transparent transparent #fff transparent; border-style:solid; border-width:5px; width:0; height:0; } 소스는 파일 첨부했습니다. 잘 살펴보시고 유용하게 이용하세요.
이온디
이온디 11년 전
<html> <body> <script> var dan = 1; var count = 3; function test(){ while( dan <= 9){ var count = 1; dan = dan + 1; } } test(); document.write(count); </script> </body> </html> var 의 있음 없음에 동작은 정상적으로 돌아갑니다. … <html> <body> <script> var dan = 1; var count = 3; function test(){ while( dan <= 9){ var count = 1; dan = dan + 1; } } test(); document.write(count); </script> </body> </html> var 의 있음 없음에 동작은 정상적으로 돌아갑니다. 하지만 자바스크립트에서는 var 를 써주느냐 안써주느냐에는 차이가 있습니다. javascript 함수를 배우셨다면 function 문을 이해하실 수 있으실텐데요 그러면 count 값을 표시하는 document.write( count); 에서 화면에는 숫자 몇이 표시될까요? function test 내의 var count = 1 의 var 를 제거하고도 테스트해보세요. 출처 : 네이버 지식인 http://kin.naver.com/qna/detail.nhn?d1id=1&dirId=1040202&docId=200069125&qb=Y291bnQg7Iqk7YGs66a97Yq4&enc=utf8&section=kin&rank=3&search_sort=0&spq=1&pid=RJHUGF5Y7uossZBev8Rsssssst0-118916&sid=U75X3nJvLDIAAEheE-o
이온디 17년 전
A라는 레이아웃을 사용하는 페이지에 접속하는 비회원을 다른 페이지로 이동하게 하는 코드입니다.즉, 로그인 한 회원만 볼 수 있게 하는 페이지를 꾸밀 때 이용할 수 있습니다. <!--@if($logged_info)--> [레이아웃] <!--@else--> <script> location.href = "http://eond.com/gateway"; </script> <!--@end--> A라는 레이아웃을 사용하는 페이지에 접속하는 비회원을 다른 페이지로 이동하게 하는 코드입니다.즉, 로그인 한 회원만 볼 수 있게 하는 페이지를 꾸밀 때 이용할 수 있습니다. <!--@if($logged_info)--> [레이아웃] <!--@else--> <script> location.href = "http://eond.com/gateway"; </script> <!--@end-->
이온디 17년 전
< script type="text/javascript">< !-- google_ad_client = "pub-1425850607942873"; google_ad_width = 125; google_ad_height = 125; google_ad_format = "125x125_as"; google_ad_type = "text"; google_ad_channel =""; google_color_border = "333333"; google_color_bg = "333333"; google_col… < script type="text/javascript">< !-- google_ad_client = "pub-1425850607942873"; google_ad_width = 125; google_ad_height = 125; google_ad_format = "125x125_as"; google_ad_type = "text"; google_ad_channel =""; google_color_border = "333333"; google_color_bg = "333333"; google_color_link = "666666"; google_color_url = "999999"; google_color_text = "666666"; //-->< /script> < script type="text/javascript" src="./files/attach/images/163354/829/162/fa6118dba0f35c1b7cc262c1e1814833.gif"> < / script>
이온디 17년 전
(클릭하면 크게 보입니다.) 어떻게 하면 잘 정리되게끔 코딩할 수 있을까요? 기존 코딩은 다음과 같습니다. (실제로 똑같지는 않고 대강의 구조가 저렇습니다.) <style> body {background:url(전체배경);} #wraper{background:url(여자배경) right bottom no-repeat;width:100%;height:100%;} #header{width:50px;} #container{} #footer{width:200px;} </style> <body> … (클릭하면 크게 보입니다.) 어떻게 하면 잘 정리되게끔 코딩할 수 있을까요? 기존 코딩은 다음과 같습니다. (실제로 똑같지는 않고 대강의 구조가 저렇습니다.) <style> body {background:url(전체배경);} #wraper{background:url(여자배경) right bottom no-repeat;width:100%;height:100%;} #header{width:50px;} #container{} #footer{width:200px;} </style> <body> <table id="wraper"> <tr> <td id="header"></td> <td id="container"></td> <td id="footer"></td> </tr> </table> 테이블로 왼쪽, 가운데, 오른쪽 나눠서 작업을 하고, 라운드 처리는 스크립트로 했었죠. 발로 코딩했는지 크로스브라우징이 되던 스크립트였는데 라운드처리도 깨지고 마진값도 제대로 안 먹히고 난장판이 돼있더군요. 그래서 새로 <div> 사용법도 어느 정도 알겠다 싶어 코딩을 시작했는데.. body {background:url(전체배경);} #wraper{background:url(여자배경) right bottom no-repeat;width:100%;height:100%;} #header{width:50px;height:100%;} #container{width:720px;height:100%;} #footer{width:200px;height:100%;} </style> <body> <div id="wraper"> <div id="header"></div> <div id="container"></div> <div id="footer"></div> </div> 대강 위와 같이 코딩을 시작했습니다. 중요한 건 #container의 높이에 따라 #header의 높이도 함께 100%가 채워져야 하는데,
이온디 17년 전
rC.addTarget('as',3,3,'#A14880','#333333',5,false); div ID, 곡선율, 배경색, 그 뒷배경색, 패딩값, 세로 길이,rC.addTarget('ID명,x축곡선율,y축곡선율,셀색깔,배경색,패딩값,세로길이(false로 할시 내용에 따라 달라짐), 어디에 지정할 것인지.. The arguments to the addTarget method are: ID of elementX radius of rounded cornerY radius of rounded co… rC.addTarget('as',3,3,'#A14880','#333333',5,false); div ID, 곡선율, 배경색, 그 뒷배경색, 패딩값, 세로 길이,rC.addTarget('ID명,x축곡선율,y축곡선율,셀색깔,배경색,패딩값,세로길이(false로 할시 내용에 따라 달라짐), 어디에 지정할 것인지.. The arguments to the addTarget method are: ID of elementX radius of rounded cornerY radius of rounded cornerBackground color of element you are applying rounded corners toBackground color of element behindPadding of content.Height of content.Where to apply corners
이온디 17년 전
이미지 없이 CSS로 둥근 모서리 구현하기(1) 간지님께서 알려주신 이 소스는 body의 배경과 div의 배경을 일치시킴으로써 라운드 효과를 내게했다. 27번째줄, 6번째줄 <html> <head> <style type="text/css"> body { font: 14px Arial, Helvetica, sans-serif; background-color: #FFFFFF; color: #FFFF66; } .rblock { background-c… 이미지 없이 CSS로 둥근 모서리 구현하기(1) 간지님께서 알려주신 이 소스는 body의 배경과 div의 배경을 일치시킴으로써 라운드 효과를 내게했다. 27번째줄, 6번째줄 <html> <head> <style type="text/css"> body { font: 14px Arial, Helvetica, sans-serif; background-color: #FFFFFF; color: #FFFF66; } .rblock { background-color: #404040; } .rblock *.m5 { margin: 0 5px; } .rblock *.m3 { margin: 0 3px; } .rblock *.m2 { margin: 0 2px; } .rblock *.m1 { margin: 0 1px; } .rblock p.content { padding: 0px 10px; } .rblocktop, .rblockbottom { background-color: #FFFFFF; /* must be of the same as body bg-color */ display: block; width: 100%; } .rblocktop *, .rblockbottom * { background-color: #404040; /* must be of the same as .rblock bg-color */ display: block; height: 1px; overflow: hidden; } </style> </head> <body> <div class="rblock" style="width: 500px;"> <div class="rblocktop"> <span class="m5"></span><span class="m3"></span><span class="m2"></span><span class="m1"></span><span class="m1"></span> </div> <p class="content" style="text-align: justify; text-indent: 0px; /* change this style to whatever you want or remove it */"> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. </p> <div class="rblockbottom"> <span class="m1"></span><span class="m1"></span><span class="m2"></span><span class="m3"></span><span class="m5"></span> </div> </div> </body> </html>