using System.Globalization; using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using MongoDB.Bson.Serialization.Attributes; using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Filters; using UGQApiServer.Models; using UGQDatabase.Models; using UGQDataAccess.Service; using UGQApiServer.Converter; using UGQApiServer.Storage; using UGQDataAccess.Repository.Models; using Microsoft.AspNetCore.Authorization; using UGQDataAccess; using UGQApiServer.Validation; using UGQApiServer.UGQData; using UGQApiServer.Auth; using JwtRoleAuthentication.Services; using System.Security.Claims; using Newtonsoft.Json.Linq; using ServerCommon; using ServerCommon.UGQ; using ServerCommon.UGQ.Models; using Microsoft.Extensions.Caching.Distributed; using Microsoft.IdentityModel.Tokens; using System.Collections.Generic; using UGQApiServer.Controllers.Common; using UGQDataAccess.Repository; using ServerCore; using ServerBase; using StackExchange.Redis; using UGQDataAccess.Logs; namespace UGQApiServer.Controllers { [Authorize] [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] [UGQWebApi] [AllowWhenSingleLogin] public class QuestEditorController : ControllerBase { AccountService _accountService; QuestEditorService _questEditorService; IStorageService _storageService; UGQMetaData _ugqMetaData; TokenService _tokenService; WebPortalTokenAuth _webPortalTokenAuth; DynamoDbClient _dynamoDbClient; readonly IConnectionMultiplexer _redis; QuestEditorApi _questEditorApi; public QuestEditorController( AccountService accountService, QuestEditorService questEditorService, IStorageService storageService, UGQMetaData uqgMetadata, TokenService tokenService, WebPortalTokenAuth webPortalTokenAuth, DynamoDbClient dynamoDbClient, QuestEditorApi questEditorApi, IConnectionMultiplexer redis) { _accountService = accountService; _questEditorService = questEditorService; _storageService = storageService; _ugqMetaData = uqgMetadata; _tokenService = tokenService; _webPortalTokenAuth = webPortalTokenAuth; _dynamoDbClient = dynamoDbClient; _questEditorApi = questEditorApi; _redis = redis; } string? userGuidFromToken() { return User.Claims.FirstOrDefault(c => c.Type == "Id")?.Value; } /// /// 퀘스트 랩 시작 페이지 /// /// pageNumber /// pageSize (최대 100) /// 검색할 상태 /// 검색할 텍스트 [HttpGet] [Route("summaries")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQSummaryList))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task getSummaries([FromQuery] int pageNumber, [FromQuery] int pageSize, [FromQuery] QuestContentState state, [FromQuery] string? searchText) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); pageSize = Math.Clamp(pageSize, 1, UGQConstants.MAX_INPUT_PAGE_SIZE); // TODO: state만 얻어오게 수정 List all = await _questEditorService.getAll(userGuid); var result = await _questEditorService.getSummaries(pageNumber, pageSize, userGuid, state, searchText); var query = all.GroupBy( p => p.State, p => p.State, (state, states) => new { Key = state, Count = states.Count(), }); var stateGroups = query.Select(x => new UGQStateGroup { State = x.Key, Count = x.Count, }).ToList(); LangEnum langEnum = Culture.ToLangEnum(CultureInfo.CurrentCulture.Name); return Results.Ok(new UGQSummaryResponse { TotalCount = all.Count, StateGroups = stateGroups, PageNumber = result.PageNumber, PageSize = result.PageSize, TotalPages = result.TotalPages, Summaries = result.Items.Select(x => x.ToDTO(langEnum)).ToList(), }); } /// /// 유저 정보 얻기 /// [HttpGet] [Route("account")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQAccountDetail))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task getAccount() { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); var accountEntity = await _accountService.getAccount(userGuid); if(accountEntity == null) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqNullEntity)); // TODO: count만 얻어오게 수정 List all = await _questEditorService.getAll(userGuid); return Results.Ok(new UGQAccountDetail { UserGuid = accountEntity.UserGuid, Nickname = accountEntity.Nickname, AccountId = accountEntity.AccountId ?? "", SlotCount = (MetaHelper.GameConfigMeta.UGQDefaultSlot + accountEntity.AdditionalSlotCount), GradeType = accountEntity.GradeType, CreatorPoint = accountEntity.CreatorPoint, TotalQuests = all.Count, }); } /// /// 유저 재화 정보 얻기 /// [HttpGet] [Route("account-currency")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQAccount))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task getAccountCurrency() { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); if (Enum.TryParse(MetaHelper.GameConfigMeta.UGQAddSlotType, true, out CurrencyType currencyType) == false) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqServerException)); double slotPrice = MetaHelper.GameConfigMeta.UGQAddSlotAmount; (var result, double currentAmount) = await CurrencyControlHelper.getMoneyByUserGuid(userGuid, currencyType); if (result.isFail()) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqCurrencyError)); return Results.Ok(new UGQAccountCurrency { CurrencyType = currencyType, Amount = currentAmount, }); } /// /// 유저의 메타버스 로그인 상태 얻기 /// [HttpGet] [Route("metaverse-online-status")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQAccount))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task getMetaverseOnlineStatus() { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); bool online = false; var game_login_cache = await _redis.GetDatabase().StringGetAsync($"login:{userGuid}"); if (game_login_cache.HasValue == true) { online = true; } return Results.Ok(new { Online = online }); } /// /// 퀘스트 슬롯을 추가하는데 필요한 재화와 소모량 얻기 /// [HttpGet] [Route("slot-price")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQSlotPrice))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task getSlotPrice() { await Task.CompletedTask; var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); // MetaHelper.GameConfigMeta.UGQDefaultSlot if (Enum.TryParse(MetaHelper.GameConfigMeta.UGQAddSlotType, true, out CurrencyType currencyType) == false) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqServerException)); double slotPrice = MetaHelper.GameConfigMeta.UGQAddSlotAmount; return Results.Ok(new UGQSlotPrice { CurrencyType = currencyType, Price = slotPrice }); } /// /// 퀘스트 슬롯 추가 /// [HttpPost] [Route("slot")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQAccount))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task addSlot() { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); if (Enum.TryParse(MetaHelper.GameConfigMeta.UGQAddSlotType, true, out CurrencyType currencyType) == false) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqServerException)); double slotPrice = MetaHelper.GameConfigMeta.UGQAddSlotAmount; var accountEntity = await _accountService.getAccount(userGuid); if (accountEntity == null) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqNullEntity)); int finalCount = MetaHelper.GameConfigMeta.UGQDefaultSlot + accountEntity.AdditionalSlotCount + 1; if (finalCount > MetaHelper.GameConfigMeta.UGQMaximumSlot) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqSlotLimit)); var result = new Result(); var db_connector = CurrencyControlHelper.getDbConnector(); (result, var is_login) = await ServerCommon.EntityHelper.isUserLoggedIn(db_connector, userGuid); if (result.isFail()) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqServerException)); if (true == is_login) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqServerException)); // 일단 슬롯 추가할 수 있음 // 게임 DB 재화 체크, 소모 (result, double currentAmount) = await CurrencyControlHelper.getMoneyByUserGuid(userGuid, currencyType); if (result.isFail()) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqCurrencyError)); if (currentAmount < slotPrice) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqNotEnoughCurrency)); (result, double updatedAmount) = await CurrencyControlHelper.changeMoneyByUserGuid(userGuid, currencyType, slotPrice); if (result.isFail()) return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqCurrencyError)); (ServerErrorCode errorCode, var updatedAccountEntity) = await _accountService.addSlotCount(userGuid, 1, accountEntity.SlotCountVersion); if (errorCode != ServerErrorCode.Success) return Results.BadRequest(ApiResponseFactory.error(errorCode)); if (updatedAccountEntity == null) { // 슬롯 추가 실패 // 슬롯수 획인, 재화 소모 처리후 SlotCountVersion이 변경되었으면 슬롯 수 변경이 저장되지 않고 실패함. // 재화는 돌려준다. (result, double rollbackAmount) = await CurrencyControlHelper.earnMoneyByUserGuid(userGuid, currencyType, slotPrice); if (result.isFail()) { Log.getLogger().error($"currency rollback error. userGuid: {userGuid}, currencyType: {currencyType}, slotPrice: {slotPrice}"); return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqCurrencyError)); } return Results.BadRequest(ApiResponseFactory.error(ServerErrorCode.UgqNullEntity)); } List business_logs = [ new UgqApiAddSlotBusinessLog(updatedAccountEntity.AdditionalSlotCount, ""), ]; var log_action = new LogActionEx(LogActionType.UgqApiAddSlot); UgqApiBusinessLogger.collectLogs(log_action, userGuid, business_logs); return Results.Ok(updatedAccountEntity.ToDTO()); } /// /// 퀘스트 생성 /// /// /// 빈 슬롯에 퀘스트 추가하는 경우에 호출 /// [HttpPost] [Route("quest")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQContent))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task addQuest([FromBody] UGQSaveQuestModel saveQuestModel) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.addQuest(userGuid, saveQuestModel); } /// /// 퀘스트 내용 가져오기 /// /// UGQSummary의 QuestContentId 값으로 api 호출 [HttpGet] [Route("quest/{questContentId}")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQContent))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task getQuest(string questContentId) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.getQuest(userGuid, questContentId); } /// /// 편집 가능한 경우 퀘스트 내용 가져오기 /// /// /// GET api와는 다르게 편집 불가능한 state인 경우에는 실패 리턴 /// /// UGQSummary의 QuestContentId 값으로 api 호출 [HttpPost] [Route("quest/{questContentId}/edit")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQContent))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task editQuest(string questContentId) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.editQuest(userGuid, questContentId); } /// /// 퀘스트 내용 업데이트 /// /// /// Task의 DialogId는 업데이트하지 않음. /// 편집 불가능 state인 경우 에러 리턴 /// /// GET 또는 edit 에서 얻은 questContentId 사용 /// 웹에서 입력한 값 [HttpPut] [Route("quest/{questContentId}")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQContent))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task saveQuest(string questContentId, [FromBody] UGQSaveQuestModel saveQuestModel) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.saveQuest(userGuid, questContentId, saveQuestModel); } /// /// 퀘스트 삭제 /// /// GET또는 edit 에서 얻은 questContentId 사용 [HttpDelete] [Route("quest/{questContentId}")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK)] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task deleteQuest(string questContentId) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.deleteQuest(userGuid, questContentId); } /// /// 퀘스트 이미지 업로드 /// /// /// 세부사항은 정리 예정 /// /// GET 또는 edit 에서 얻은 questContentId 사용 /// 기본 이미지 인덱스 /// 기본 이미지 인덱스 /// 업로드 파일 /// 업로드 파일 [HttpPost] [Route("quest-image/{questContentId}")] [Produces("application/json")] public async Task uploadQuestImage(string questContentId, [FromQuery] int? titleImageId, [FromQuery] int? bannerImageId, IFormFile? title, IFormFile? banner) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.uploadQuestImage(userGuid, questContentId, titleImageId ?? 0, bannerImageId ?? 0, title, banner); } /// /// 퀘스트 타이틀 이미지 업로드 /// /// /// 세부사항은 정리 예정 /// /// GET 또는 edit 에서 얻은 questContentId 사용 /// 기본 이미지 인덱스 /// 업로드 파일 [HttpPost] [Route("quest-title-image/{questContentId}")] [Produces("application/json")] public async Task uploadQuestTitleImage(string questContentId, [FromQuery] int? titleImageId, IFormFile? title) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.uploadQuestImage(userGuid, questContentId, titleImageId ?? 0, 0, title, null); } /// /// 퀘스트 배너 이미지 업로드 /// /// /// 세부사항은 정리 예정 /// /// GET 또는 edit 에서 얻은 questContentId 사용 /// 기본 이미지 인덱스 /// 업로드 파일 [HttpPost] [Route("quest-banner-image/{questContentId}")] [Produces("application/json")] public async Task uploadQuestBannerImage(string questContentId, [FromQuery] int? bannerImageId, IFormFile? banner) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.uploadQuestImage(userGuid, questContentId, 0, bannerImageId ?? 0, null, banner); } /// /// 퀘스트 다이얼로그 가져오기 /// /// GET 또는 edit 에서 얻은 questContentId 사용 /// UGQContent.Tasks에 있는 DialogId 사용 [HttpGet] [Route("quest-dialog/{questContentId}/{dialogId}")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQDialog))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task getQuestDialog(string questContentId, string dialogId) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.getQuestDialog(userGuid, questContentId, dialogId); } /// /// 퀘스트 다이얼로그 새로 생성 /// /// /// Dialog 를 추가하고, UGQContent.Tasks의 DialogId를 저장 /// /// GET 또는 edit 에서 얻은 questContentId 사용 /// 몇번째 Task에 추가되는 다이얼로그인지. 0부터 시작 /// questDialog [HttpPost] [Route("quest-dialog/{questContentId}")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQAddQuestDialogResponse))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task addQuestDialog(string questContentId, [FromQuery] int taskIndex, [FromBody] UGQSaveDialogModel questDialog) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.addQuestDialog(userGuid, questContentId, taskIndex, questDialog); } /// /// 퀘스트 다이얼로그 업데이트 /// /// GET 또는 edit 에서 얻은 questContentId 사용 /// UGQContent.Tasks에 있는 DialogId 사용 /// /// questDialog [HttpPut] [Route("quest-dialog/{questContentId}/{dialogId}")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQDialog))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task saveQuestDialog(string questContentId, string dialogId, [FromBody] UGQSaveDialogModel questDialog) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.saveQuestDialog(userGuid, questContentId, dialogId, questDialog); } /// /// 퀘스트 state 변경 /// /// /// Test 로 변경 실패 시 ValidationErrorResponse로 응답 /// /// GET또는 edit 에서 얻은 questContentId 사용 /// QuestContentState [HttpPatch] [Route("quest-state/{questContentId}")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQContent))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ValidationErrorResponse))] public async Task changeQuestState(string questContentId, [FromQuery] QuestContentState state) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.changeQuestState(userGuid, questContentId, state); } /// /// CreatorPoint 내역 보기 /// /// 페이지 /// 페이지 사이즈 (최대 100) /// kind /// 검색할 시작 시간 /// 검색할 종료 시간 [HttpGet] [Route("creator-point-history")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQCreatorPointHistoryResult))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task getCreatorPointHistory([FromQuery] int pageNumber, [FromQuery] int pageSize, CreatorPointHistoryKind kind, DateTimeOffset startDate, DateTimeOffset endDate) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.getCreatorPointHistory(userGuid, pageNumber, pageSize, kind, startDate, endDate); } /// /// 퀘스트별 수익 보기 /// /// 페이지 /// 페이지 사이즈 (최대 100) [HttpGet] [Route("quest-profit-stats")] [Produces("application/json")] [SwaggerResponse(StatusCodes.Status200OK, Type = typeof(UGQQuestProfitStatsResult))] [SwaggerResponse(StatusCodes.Status400BadRequest, Type = typeof(ApiErrorResponse))] public async Task getQuestProfitStats([FromQuery] int pageNumber, [FromQuery] int pageSize) { var userGuid = userGuidFromToken(); if (userGuid == null) return Results.Unauthorized(); return await _questEditorApi.getQuestProfitStats(userGuid, pageNumber, pageSize); } } }