package apperror import ( "errors" "fmt" "strings" "github.com/go-sql-driver/mysql" ) // AppError is a structured application error with an HTTP status code. // JSON response format: {"error": "", "message": ""} type AppError struct { Code string `json:"error"` Message string `json:"message"` Status int `json:"-"` } func (e *AppError) Error() string { return e.Message } // New creates a new AppError. func New(code string, message string, status int) *AppError { return &AppError{Code: code, Message: message, Status: status} } // Wrap creates a new AppError that wraps a cause error. func Wrap(code string, message string, status int, cause error) *AppError { return &AppError{Code: code, Message: fmt.Sprintf("%s: %v", message, cause), Status: status} } // Common errors var ( ErrBadRequest = &AppError{Code: "bad_request", Message: "잘못된 요청입니다", Status: 400} ErrUnauthorized = &AppError{Code: "unauthorized", Message: "인증이 필요합니다", Status: 401} ErrForbidden = &AppError{Code: "forbidden", Message: "권한이 없습니다", Status: 403} ErrNotFound = &AppError{Code: "not_found", Message: "리소스를 찾을 수 없습니다", Status: 404} ErrConflict = &AppError{Code: "conflict", Message: "이미 존재합니다", Status: 409} ErrRateLimited = &AppError{Code: "rate_limited", Message: "요청이 너무 많습니다", Status: 429} ErrInternal = &AppError{Code: "internal_error", Message: "서버 오류가 발생했습니다", Status: 500} ) // BadRequest creates a 400 error with a custom message. func BadRequest(message string) *AppError { return &AppError{Code: "bad_request", Message: message, Status: 400} } // Unauthorized creates a 401 error with a custom message. func Unauthorized(message string) *AppError { return &AppError{Code: "unauthorized", Message: message, Status: 401} } // NotFound creates a 404 error with a custom message. func NotFound(message string) *AppError { return &AppError{Code: "not_found", Message: message, Status: 404} } // Conflict creates a 409 error with a custom message. func Conflict(message string) *AppError { return &AppError{Code: "conflict", Message: message, Status: 409} } // Internal creates a 500 error with a custom message. func Internal(message string) *AppError { return &AppError{Code: "internal_error", Message: message, Status: 500} } // ErrDuplicateUsername is returned when a username already exists. var ErrDuplicateUsername = fmt.Errorf("이미 사용 중인 아이디입니다") // IsDuplicateEntry checks if a GORM error is a MySQL duplicate key violation (error 1062). func IsDuplicateEntry(err error) bool { var mysqlErr *mysql.MySQLError if errors.As(err, &mysqlErr) { return mysqlErr.Number == 1062 } return strings.Contains(err.Error(), "Duplicate entry") || strings.Contains(err.Error(), "UNIQUE constraint") }